@rapidmx/meet-plugin 0.1.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/LICENSE +382 -0
- package/README.md +38 -0
- package/apps/meet/[token].tsx +144 -0
- package/apps/meet/_CallControls.tsx +69 -0
- package/apps/meet/_CallView.tsx +338 -0
- package/apps/meet/_MeetChrome.tsx +30 -0
- package/apps/meet/_MeetLobby.tsx +223 -0
- package/apps/meet/_ParticipantTile.tsx +66 -0
- package/apps/meet/_layout.tsx +34 -0
- package/apps/meet/_meetApi.ts +66 -0
- package/apps/meet/index.tsx +22 -0
- package/apps/settings-video-conferencing/_PersonalRoomCard.tsx +140 -0
- package/apps/settings-video-conferencing/_layout.tsx +47 -0
- package/apps/settings-video-conferencing/index.tsx +192 -0
- package/apps/shared/media/activeSpeaker.ts +45 -0
- package/apps/shared/media/deviceMedia.ts +141 -0
- package/apps/shared/media/levelMeter.ts +91 -0
- package/apps/shared/push/GuestSignalingClient.ts +338 -0
- package/apps/shared/webrtc/MeshConnectionManager.ts +350 -0
- package/apps/shared/webrtc/realPeerConnection.ts +37 -0
- package/apps/shared/webrtc/types.ts +90 -0
- package/dist/apps/meet/[token].d.ts +33 -0
- package/dist/apps/meet/[token].js +101 -0
- package/dist/apps/meet/_CallControls.d.ts +18 -0
- package/dist/apps/meet/_CallControls.js +6 -0
- package/dist/apps/meet/_CallView.d.ts +41 -0
- package/dist/apps/meet/_CallView.js +225 -0
- package/dist/apps/meet/_MeetChrome.d.ts +13 -0
- package/dist/apps/meet/_MeetChrome.js +10 -0
- package/dist/apps/meet/_MeetLobby.d.ts +29 -0
- package/dist/apps/meet/_MeetLobby.js +120 -0
- package/dist/apps/meet/_ParticipantTile.d.ts +18 -0
- package/dist/apps/meet/_ParticipantTile.js +25 -0
- package/dist/apps/meet/_layout.d.ts +9 -0
- package/dist/apps/meet/_layout.js +8 -0
- package/dist/apps/meet/_meetApi.d.ts +47 -0
- package/dist/apps/meet/_meetApi.js +23 -0
- package/dist/apps/meet/index.d.ts +5 -0
- package/dist/apps/meet/index.js +8 -0
- package/dist/apps/settings-video-conferencing/_PersonalRoomCard.d.ts +24 -0
- package/dist/apps/settings-video-conferencing/_PersonalRoomCard.js +77 -0
- package/dist/apps/settings-video-conferencing/_layout.d.ts +16 -0
- package/dist/apps/settings-video-conferencing/_layout.js +8 -0
- package/dist/apps/settings-video-conferencing/index.d.ts +16 -0
- package/dist/apps/settings-video-conferencing/index.js +92 -0
- package/dist/apps/shared/media/activeSpeaker.d.ts +20 -0
- package/dist/apps/shared/media/activeSpeaker.js +39 -0
- package/dist/apps/shared/media/deviceMedia.d.ts +65 -0
- package/dist/apps/shared/media/deviceMedia.js +110 -0
- package/dist/apps/shared/media/levelMeter.d.ts +40 -0
- package/dist/apps/shared/media/levelMeter.js +61 -0
- package/dist/apps/shared/push/GuestSignalingClient.d.ts +89 -0
- package/dist/apps/shared/push/GuestSignalingClient.js +291 -0
- package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +97 -0
- package/dist/apps/shared/webrtc/MeshConnectionManager.js +262 -0
- package/dist/apps/shared/webrtc/realPeerConnection.d.ts +7 -0
- package/dist/apps/shared/webrtc/realPeerConnection.js +26 -0
- package/dist/apps/shared/webrtc/types.d.ts +94 -0
- package/dist/apps/shared/webrtc/types.js +9 -0
- package/dist/lib/index.js +25 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/models/mongo/VideoMeetingInviteeMongo.js +87 -0
- package/dist/lib/models/mongo/VideoMeetingInviteeMongo.js.map +1 -0
- package/dist/lib/models/mongo/VideoMeetingMongo.js +126 -0
- package/dist/lib/models/mongo/VideoMeetingMongo.js.map +1 -0
- package/dist/lib/models/sql/VideoMeetingInviteeSQL.js +87 -0
- package/dist/lib/models/sql/VideoMeetingInviteeSQL.js.map +1 -0
- package/dist/lib/models/sql/VideoMeetingSQL.js +127 -0
- package/dist/lib/models/sql/VideoMeetingSQL.js.map +1 -0
- package/dist/lib/models/types.js +19 -0
- package/dist/lib/models/types.js.map +1 -0
- package/dist/lib/mongo.js +13 -0
- package/dist/lib/mongo.js.map +1 -0
- package/dist/lib/routes/BaseVideoMeetingRoute.js +743 -0
- package/dist/lib/routes/BaseVideoMeetingRoute.js.map +1 -0
- package/dist/lib/routes/mongo/VideoMeetingRouteMongo.js +31 -0
- package/dist/lib/routes/mongo/VideoMeetingRouteMongo.js.map +1 -0
- package/dist/lib/routes/sql/VideoMeetingRouteSQL.js +31 -0
- package/dist/lib/routes/sql/VideoMeetingRouteSQL.js.map +1 -0
- package/dist/lib/sql.js +13 -0
- package/dist/lib/sql.js.map +1 -0
- package/dist/lib/util/BookingIntegrationUtils.js +115 -0
- package/dist/lib/util/BookingIntegrationUtils.js.map +1 -0
- package/dist/lib/util/IceServerUtils.js +68 -0
- package/dist/lib/util/IceServerUtils.js.map +1 -0
- package/dist/lib/util/PublicUrlUtils.js +37 -0
- package/dist/lib/util/PublicUrlUtils.js.map +1 -0
- package/dist/lib/util/RouteAccessUtils.js +32 -0
- package/dist/lib/util/RouteAccessUtils.js.map +1 -0
- package/dist/lib/util/TokenUtils.js +33 -0
- package/dist/lib/util/TokenUtils.js.map +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/models/mongo/VideoMeetingInviteeMongo.d.ts +16 -0
- package/dist/types/models/mongo/VideoMeetingMongo.d.ts +23 -0
- package/dist/types/models/sql/VideoMeetingInviteeSQL.d.ts +16 -0
- package/dist/types/models/sql/VideoMeetingSQL.d.ts +23 -0
- package/dist/types/models/types.d.ts +131 -0
- package/dist/types/mongo.d.ts +8 -0
- package/dist/types/routes/BaseVideoMeetingRoute.d.ts +368 -0
- package/dist/types/routes/mongo/VideoMeetingRouteMongo.d.ts +10 -0
- package/dist/types/routes/sql/VideoMeetingRouteSQL.d.ts +10 -0
- package/dist/types/sql.d.ts +8 -0
- package/dist/types/util/BookingIntegrationUtils.d.ts +51 -0
- package/dist/types/util/IceServerUtils.d.ts +57 -0
- package/dist/types/util/PublicUrlUtils.d.ts +11 -0
- package/dist/types/util/RouteAccessUtils.d.ts +8 -0
- package/dist/types/util/TokenUtils.d.ts +23 -0
- package/package.json +181 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/** The lexicographically smaller uid is always the offerer for that pair - see this module's doc comment. */
|
|
2
|
+
export function isOfferer(selfUid, peerUid) {
|
|
3
|
+
return selfUid < peerUid;
|
|
4
|
+
}
|
|
5
|
+
export class MeshConnectionManager {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.peers = new Map();
|
|
9
|
+
this.listeners = new Set();
|
|
10
|
+
this.started = false;
|
|
11
|
+
this.stopped = false;
|
|
12
|
+
}
|
|
13
|
+
get participants() {
|
|
14
|
+
return [...this.peers.values()].map((peer) => ({ uid: peer.uid, name: peer.name }));
|
|
15
|
+
}
|
|
16
|
+
get presenterUid() {
|
|
17
|
+
return this.currentPresenterUid;
|
|
18
|
+
}
|
|
19
|
+
onEvent(listener) {
|
|
20
|
+
this.listeners.add(listener);
|
|
21
|
+
return () => this.listeners.delete(listener);
|
|
22
|
+
}
|
|
23
|
+
/** Subscribes to the channel and announces this participant. Idempotent - a second call is a no-op. */
|
|
24
|
+
start() {
|
|
25
|
+
if (this.started) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.started = true;
|
|
29
|
+
this.unsubscribe = this.options.channel.onMessage((message) => this.handleMessage(message));
|
|
30
|
+
this.sendHello();
|
|
31
|
+
}
|
|
32
|
+
/** Announces departure, closes every peer connection and unsubscribes - idempotent, and safe to call whether
|
|
33
|
+
* or not `start()` ever ran. Never removes the camera/microphone indicator itself - stopping `localStream`'s
|
|
34
|
+
* own tracks is the caller's job (see `deviceMedia.ts`'s `stopStream()`), since this manager never owns that
|
|
35
|
+
* stream's lifecycle, only attaches it. */
|
|
36
|
+
stop() {
|
|
37
|
+
if (this.stopped) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.stopped = true;
|
|
41
|
+
if (this.started) {
|
|
42
|
+
this.send({ kind: "bye" });
|
|
43
|
+
}
|
|
44
|
+
this.unsubscribe?.();
|
|
45
|
+
this.unsubscribe = undefined;
|
|
46
|
+
for (const peer of this.peers.values()) {
|
|
47
|
+
peer.pc.close();
|
|
48
|
+
}
|
|
49
|
+
this.peers.clear();
|
|
50
|
+
this.listeners.clear();
|
|
51
|
+
}
|
|
52
|
+
/** Swaps every peer connection's outgoing video track (presentation mode) - see this module's doc comment.
|
|
53
|
+
* `track` is `null` to stop sending video at all (never used by this plugin's UI today, which always has a
|
|
54
|
+
* camera-or-screen track to fall back to, but kept correct since `RTCRtpSender.replaceTrack()` itself allows
|
|
55
|
+
* it). */
|
|
56
|
+
replaceLocalVideoTrack(track) {
|
|
57
|
+
for (const peer of this.peers.values()) {
|
|
58
|
+
const sender = peer.pc.getSenders().find((s) => s.track?.kind === "video");
|
|
59
|
+
void sender?.replaceTrack(track);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Claims presenter status for the local participant. Refuses (returns `false`, sends nothing) when someone
|
|
63
|
+
* else already presents - the caller (`_CallView.tsx`) uses this to disable its own "share screen" control
|
|
64
|
+
* rather than let a claim silently do nothing. */
|
|
65
|
+
claimPresenter() {
|
|
66
|
+
if (this.currentPresenterUid !== undefined && this.currentPresenterUid !== this.options.selfUid) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
this.currentPresenterUid = this.options.selfUid;
|
|
70
|
+
this.send({ kind: "presenter-claim" });
|
|
71
|
+
this.emit({ type: "presenter-changed", uid: this.currentPresenterUid });
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/** Releases presenter status - a no-op unless the local participant currently holds it. */
|
|
75
|
+
releasePresenter() {
|
|
76
|
+
if (this.currentPresenterUid !== this.options.selfUid) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.currentPresenterUid = undefined;
|
|
80
|
+
this.send({ kind: "presenter-release" });
|
|
81
|
+
this.emit({ type: "presenter-changed", uid: undefined });
|
|
82
|
+
}
|
|
83
|
+
sendHello() {
|
|
84
|
+
this.send({ kind: "hello", name: this.options.selfName });
|
|
85
|
+
}
|
|
86
|
+
send(partial) {
|
|
87
|
+
this.options.channel.send({ type: "video-meeting-signal", from: this.options.selfUid, ...partial });
|
|
88
|
+
}
|
|
89
|
+
emit(event) {
|
|
90
|
+
for (const listener of [...this.listeners]) {
|
|
91
|
+
listener(event);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
handleMessage(message) {
|
|
95
|
+
if (message.type !== "video-meeting-signal" || message.from === this.options.selfUid) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (message.to !== undefined && message.to !== this.options.selfUid) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
switch (message.kind) {
|
|
102
|
+
case "hello":
|
|
103
|
+
this.handleHello(message);
|
|
104
|
+
return;
|
|
105
|
+
case "bye":
|
|
106
|
+
this.handleBye(message.from);
|
|
107
|
+
return;
|
|
108
|
+
case "offer":
|
|
109
|
+
void this.handleOffer(message);
|
|
110
|
+
return;
|
|
111
|
+
case "answer":
|
|
112
|
+
void this.handleAnswer(message);
|
|
113
|
+
return;
|
|
114
|
+
case "ice-candidate":
|
|
115
|
+
void this.handleIceCandidate(message);
|
|
116
|
+
return;
|
|
117
|
+
case "presenter-claim":
|
|
118
|
+
this.handlePresenterClaim(message.from);
|
|
119
|
+
return;
|
|
120
|
+
case "presenter-release":
|
|
121
|
+
this.handlePresenterRelease(message.from);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
handleHello(message) {
|
|
126
|
+
if (this.peers.has(message.from)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const name = message.name ?? message.from;
|
|
130
|
+
const peer = this.createPeer(message.from, name);
|
|
131
|
+
this.emit({ type: "participant-joined", participant: { uid: peer.uid, name: peer.name } });
|
|
132
|
+
// Let a newcomer who couldn't have seen our own original `hello` learn about us too - see this module's
|
|
133
|
+
// doc comment on roster discovery.
|
|
134
|
+
this.sendHello();
|
|
135
|
+
if (isOfferer(this.options.selfUid, message.from)) {
|
|
136
|
+
void this.initiateOffer(peer);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
handleBye(uid) {
|
|
140
|
+
const peer = this.peers.get(uid);
|
|
141
|
+
if (!peer) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
peer.pc.close();
|
|
145
|
+
this.peers.delete(uid);
|
|
146
|
+
this.emit({ type: "participant-left", uid });
|
|
147
|
+
if (this.currentPresenterUid === uid) {
|
|
148
|
+
this.currentPresenterUid = undefined;
|
|
149
|
+
this.emit({ type: "presenter-changed", uid: undefined });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
createPeer(uid, name) {
|
|
153
|
+
const pc = this.options.createPeerConnection({ iceServers: this.options.iceServers });
|
|
154
|
+
const peer = { uid, name, pc, remoteDescriptionSet: false, pendingCandidates: [] };
|
|
155
|
+
this.peers.set(uid, peer);
|
|
156
|
+
for (const track of this.options.localStream.getTracks()) {
|
|
157
|
+
pc.addTrack(track, this.options.localStream);
|
|
158
|
+
}
|
|
159
|
+
pc.onicecandidate = (event) => {
|
|
160
|
+
if (event.candidate) {
|
|
161
|
+
this.send({ kind: "ice-candidate", to: uid, candidate: event.candidate });
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
pc.ontrack = (event) => {
|
|
165
|
+
const stream = event.streams[0];
|
|
166
|
+
if (stream) {
|
|
167
|
+
this.emit({ type: "remote-stream", uid, stream });
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
pc.onconnectionstatechange = () => {
|
|
171
|
+
if (pc.connectionState === "failed" || pc.connectionState === "closed") {
|
|
172
|
+
this.handleBye(uid);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
return peer;
|
|
176
|
+
}
|
|
177
|
+
async initiateOffer(peer) {
|
|
178
|
+
const offer = await peer.pc.createOffer();
|
|
179
|
+
await peer.pc.setLocalDescription(offer);
|
|
180
|
+
this.send({ kind: "offer", to: peer.uid, sdp: offer });
|
|
181
|
+
}
|
|
182
|
+
async handleOffer(message) {
|
|
183
|
+
if (!message.sdp) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let peer = this.peers.get(message.from);
|
|
187
|
+
const isNew = !peer;
|
|
188
|
+
if (!peer) {
|
|
189
|
+
peer = this.createPeer(message.from, message.from);
|
|
190
|
+
}
|
|
191
|
+
await peer.pc.setRemoteDescription(message.sdp);
|
|
192
|
+
this.flushPendingCandidates(peer);
|
|
193
|
+
if (isNew) {
|
|
194
|
+
this.emit({ type: "participant-joined", participant: { uid: peer.uid, name: peer.name } });
|
|
195
|
+
}
|
|
196
|
+
const answer = await peer.pc.createAnswer();
|
|
197
|
+
await peer.pc.setLocalDescription(answer);
|
|
198
|
+
this.send({ kind: "answer", to: peer.uid, sdp: answer });
|
|
199
|
+
}
|
|
200
|
+
async handleAnswer(message) {
|
|
201
|
+
const peer = this.peers.get(message.from);
|
|
202
|
+
if (!peer || !message.sdp) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await peer.pc.setRemoteDescription(message.sdp);
|
|
206
|
+
this.flushPendingCandidates(peer);
|
|
207
|
+
}
|
|
208
|
+
async handleIceCandidate(message) {
|
|
209
|
+
if (!message.candidate) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const peer = this.peers.get(message.from);
|
|
213
|
+
if (!peer) {
|
|
214
|
+
// No connection tracked for this peer yet (a candidate that raced ahead of its own offer/answer) -
|
|
215
|
+
// nothing to buffer it against, and one lost trickle-ICE candidate does not by itself break a
|
|
216
|
+
// connection (there are normally several). See this module's known-limitations note in the Phase 2
|
|
217
|
+
// report.
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!peer.remoteDescriptionSet) {
|
|
221
|
+
peer.pendingCandidates.push(message.candidate);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await peer.pc.addIceCandidate(message.candidate);
|
|
225
|
+
}
|
|
226
|
+
flushPendingCandidates(peer) {
|
|
227
|
+
peer.remoteDescriptionSet = true;
|
|
228
|
+
const pending = peer.pendingCandidates;
|
|
229
|
+
peer.pendingCandidates = [];
|
|
230
|
+
for (const candidate of pending) {
|
|
231
|
+
void peer.pc.addIceCandidate(candidate);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
handlePresenterClaim(from) {
|
|
235
|
+
if (this.currentPresenterUid === undefined) {
|
|
236
|
+
this.currentPresenterUid = from;
|
|
237
|
+
this.emit({ type: "presenter-changed", uid: from });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (this.currentPresenterUid === from) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// Collision: a claim from someone other than the presenter we already recorded. Resolve deterministically
|
|
244
|
+
// - see this module's doc comment - so every participant converges on the same winner. When the
|
|
245
|
+
// already-recorded presenter has the smaller uid, it wins and nothing changes here - including when that
|
|
246
|
+
// presenter is `this.options.selfUid` itself, whose own optimistic claim simply stays in place.
|
|
247
|
+
if (from < this.currentPresenterUid) {
|
|
248
|
+
const loser = this.currentPresenterUid;
|
|
249
|
+
this.currentPresenterUid = from;
|
|
250
|
+
this.emit({ type: "presenter-changed", uid: from });
|
|
251
|
+
if (loser === this.options.selfUid) {
|
|
252
|
+
this.send({ kind: "presenter-release" });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
handlePresenterRelease(from) {
|
|
257
|
+
if (this.currentPresenterUid === from) {
|
|
258
|
+
this.currentPresenterUid = undefined;
|
|
259
|
+
this.emit({ type: "presenter-changed", uid: undefined });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Adapts the real browser `RTCPeerConnection` to `RTCPeerConnectionLike` (`types.ts`) - `MeshConnectionManager`'s
|
|
2
|
+
* own tests never use this (they inject a fake `RTCPeerConnectionLike` directly); `apps/meet/_CallView.tsx` is
|
|
3
|
+
* this factory's only real caller. `createBrowserPeerConnection` is a plain function value, never invoked at
|
|
4
|
+
* module scope, so importing this file is safe during this plugin's page SSR, where `RTCPeerConnection` does not
|
|
5
|
+
* exist at all - it would only throw if actually *called* outside a browser. */
|
|
6
|
+
import type { RTCPeerConnectionFactory } from "./types.js";
|
|
7
|
+
export declare const createBrowserPeerConnection: RTCPeerConnectionFactory;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const createBrowserPeerConnection = (config) => {
|
|
2
|
+
const pc = new RTCPeerConnection(config);
|
|
3
|
+
const like = {
|
|
4
|
+
addTrack: (track, stream) => pc.addTrack(track, stream),
|
|
5
|
+
getSenders: () => pc.getSenders(),
|
|
6
|
+
createOffer: () => pc.createOffer(),
|
|
7
|
+
createAnswer: () => pc.createAnswer(),
|
|
8
|
+
setLocalDescription: (description) => pc.setLocalDescription(description),
|
|
9
|
+
setRemoteDescription: (description) => pc.setRemoteDescription(description),
|
|
10
|
+
addIceCandidate: (candidate) => pc.addIceCandidate(candidate),
|
|
11
|
+
close: () => pc.close(),
|
|
12
|
+
onicecandidate: null,
|
|
13
|
+
ontrack: null,
|
|
14
|
+
onconnectionstatechange: null,
|
|
15
|
+
get connectionState() {
|
|
16
|
+
return pc.connectionState;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
// Forwards the real browser events to whatever handler `MeshConnectionManager` has currently assigned on
|
|
20
|
+
// `like` - a level of indirection needed because `like`'s own handler properties are reassigned *after* this
|
|
21
|
+
// adapter object is constructed and returned (see `MeshConnectionManager.createPeer()`).
|
|
22
|
+
pc.onicecandidate = (event) => like.onicecandidate?.({ candidate: event.candidate ? event.candidate.toJSON() : null });
|
|
23
|
+
pc.ontrack = (event) => like.ontrack?.({ streams: event.streams });
|
|
24
|
+
pc.onconnectionstatechange = () => like.onconnectionstatechange?.();
|
|
25
|
+
return like;
|
|
26
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Shared types for `MeshConnectionManager.ts` and its signaling transport (`apps/shared/push/GuestSignalingClient.ts`) -
|
|
2
|
+
* kept in their own module so a test can depend on the shapes alone. `GuestSignalingClient` itself now also serves
|
|
3
|
+
* an already-authenticated real caller (`VideoMeetingJoinResult.authenticated`), not only a synthetic guest - see
|
|
4
|
+
* its own doc comment. */
|
|
5
|
+
/**
|
|
6
|
+
* One WebRTC signaling message, published as the JSON body of `POST /push/:meetingUid` (`BasePushRoute.send()`)
|
|
7
|
+
* and received back over `/push`'s WebSocket, exactly as `BaseVideoMeetingRoute`'s doc comment describes ("SDP
|
|
8
|
+
* offers/answers and ICE candidates are ordinary `NotificationUtils.sendMessage()` payloads"). `type` is a fixed
|
|
9
|
+
* literal so a listener on the meeting's channel (which will also see other frame shapes if this server ever
|
|
10
|
+
* grows other push consumers) can cheaply recognize and skip anything that isn't one of these.
|
|
11
|
+
*
|
|
12
|
+
* `hello`/`bye`/`presenter-claim`/`presenter-release` are broadcast (`to` left unset - every participant is
|
|
13
|
+
* relevant); `offer`/`answer`/`ice-candidate` are point-to-point (`to` is the intended peer's uid) - a recipient
|
|
14
|
+
* that isn't `to` (when set) ignores the message, since a channel's pub/sub fans every message out to every
|
|
15
|
+
* subscriber, sender included.
|
|
16
|
+
*/
|
|
17
|
+
export interface SignalMessage {
|
|
18
|
+
type: "video-meeting-signal";
|
|
19
|
+
kind: "hello" | "bye" | "offer" | "answer" | "ice-candidate" | "presenter-claim" | "presenter-release";
|
|
20
|
+
/** The sender's uid - the real, already-authenticated caller's own uid when `join()` returned
|
|
21
|
+
* `authenticated: true`, else the `guest:<random>` uid `BaseVideoMeetingRoute.join()` minted (either way, see
|
|
22
|
+
* `VideoMeetingJoinResult.selfUid`). */
|
|
23
|
+
from: string;
|
|
24
|
+
/** Set only on a point-to-point message (`offer`/`answer`/`ice-candidate`). */
|
|
25
|
+
to?: string;
|
|
26
|
+
/** `hello` only - the display name the sender chose in the lobby. */
|
|
27
|
+
name?: string;
|
|
28
|
+
/** `offer`/`answer` only. */
|
|
29
|
+
sdp?: RTCSessionDescriptionInit;
|
|
30
|
+
/** `ice-candidate` only. */
|
|
31
|
+
candidate?: RTCIceCandidateInit;
|
|
32
|
+
}
|
|
33
|
+
/** What `MeshConnectionManager` needs from a signaling transport - implemented by
|
|
34
|
+
* `apps/shared/push/GuestSignalingClient.ts` for a real call, and by an in-memory fake for `MeshConnectionManager`'s own
|
|
35
|
+
* tests (see `test/webrtc/MeshConnectionManager.test.ts`), which never opens a real network connection. */
|
|
36
|
+
export interface SignalingChannel {
|
|
37
|
+
send(message: SignalMessage): void;
|
|
38
|
+
/** Calls `handler` with every message received on the channel (including, per the pub/sub fan-out this
|
|
39
|
+
* transport is built on, this same client's own sends - `MeshConnectionManager` filters those out itself
|
|
40
|
+
* rather than relying on the transport to). Returns the function that stops listening. */
|
|
41
|
+
onMessage(handler: (message: SignalMessage) => void): () => void;
|
|
42
|
+
}
|
|
43
|
+
/** The subset of `RTCRtpSender` `MeshConnectionManager.replaceLocalVideoTrack()` uses (presentation mode - see
|
|
44
|
+
* this plugin's Phase 2 `.claude/NOTES.md` entry: sharing replaces the outgoing camera track rather than adding a
|
|
45
|
+
* second video track, so no renegotiation is ever needed). A real `RTCRtpSender` already satisfies this shape. */
|
|
46
|
+
export interface RTCRtpSenderLike {
|
|
47
|
+
track: MediaStreamTrack | null;
|
|
48
|
+
replaceTrack(track: MediaStreamTrack | null): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/** The subset of `RTCPeerConnection` `MeshConnectionManager` uses - what a test's fake implements, and what
|
|
51
|
+
* `apps/shared/webrtc/realPeerConnection.ts` adapts the real browser constructor to. Event handlers are plain settable
|
|
52
|
+
* properties (`RTCPeerConnection`'s own `on*` style) rather than `addEventListener`, matching the browser API
|
|
53
|
+
* this stands in for and keeping a test fake to the bare minimum. */
|
|
54
|
+
export interface RTCPeerConnectionLike {
|
|
55
|
+
addTrack(track: MediaStreamTrack, stream: MediaStream): RTCRtpSenderLike;
|
|
56
|
+
getSenders(): RTCRtpSenderLike[];
|
|
57
|
+
createOffer(): Promise<RTCSessionDescriptionInit>;
|
|
58
|
+
createAnswer(): Promise<RTCSessionDescriptionInit>;
|
|
59
|
+
setLocalDescription(description: RTCSessionDescriptionInit): Promise<void>;
|
|
60
|
+
setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void>;
|
|
61
|
+
addIceCandidate(candidate: RTCIceCandidateInit): Promise<void>;
|
|
62
|
+
close(): void;
|
|
63
|
+
onicecandidate: ((event: {
|
|
64
|
+
candidate: RTCIceCandidateInit | null;
|
|
65
|
+
}) => void) | null;
|
|
66
|
+
ontrack: ((event: {
|
|
67
|
+
streams: readonly MediaStream[];
|
|
68
|
+
}) => void) | null;
|
|
69
|
+
onconnectionstatechange: (() => void) | null;
|
|
70
|
+
connectionState: string;
|
|
71
|
+
}
|
|
72
|
+
export type RTCPeerConnectionFactory = (config: {
|
|
73
|
+
iceServers: RTCIceServer[];
|
|
74
|
+
}) => RTCPeerConnectionLike;
|
|
75
|
+
/** One other participant currently known to be in the call - `MeshConnectionManager.participants` never includes
|
|
76
|
+
* the local participant themselves (the UI already knows its own name/uid without asking the manager). */
|
|
77
|
+
export interface MeshParticipant {
|
|
78
|
+
uid: string;
|
|
79
|
+
name: string;
|
|
80
|
+
}
|
|
81
|
+
export type MeshEvent = {
|
|
82
|
+
type: "participant-joined";
|
|
83
|
+
participant: MeshParticipant;
|
|
84
|
+
} | {
|
|
85
|
+
type: "participant-left";
|
|
86
|
+
uid: string;
|
|
87
|
+
} | {
|
|
88
|
+
type: "remote-stream";
|
|
89
|
+
uid: string;
|
|
90
|
+
stream: MediaStream;
|
|
91
|
+
} | {
|
|
92
|
+
type: "presenter-changed";
|
|
93
|
+
uid: string | undefined;
|
|
94
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* Private and public WebRTC video conferencing for a `@rapidmx/restapi`-based mail server: a mailbox owner creates
|
|
7
|
+
* a `VideoMeeting` (private, with one join link per invitee, or public, with a single shareable link), and a
|
|
8
|
+
* participant - the owner or an anonymous guest holding a valid link - joins it and exchanges WebRTC signaling
|
|
9
|
+
* over the server's existing `/push` channel (see `BaseVideoMeetingRoute`'s class doc comment).
|
|
10
|
+
*
|
|
11
|
+
* This module exports only the backend-agnostic surface: the entity interfaces, the pure ICE-server/public-URL/
|
|
12
|
+
* token utilities, the abstract route, and `createSingleInviteeVideoMeeting()` - the small integration function
|
|
13
|
+
* another plugin (`booking-plugin`, today) calls in-process to mint a private meeting without either package
|
|
14
|
+
* taking a hard dependency on the other (see `util/BookingIntegrationUtils.ts`'s doc comment). The concrete
|
|
15
|
+
* Mongo/SQL classes a server loads (models and the route mounted at `/api/mail/video-meetings`) come from this
|
|
16
|
+
* package's `./mongo` and `./sql` entry points.
|
|
17
|
+
*/
|
|
18
|
+
export * from "./models/types.js";
|
|
19
|
+
export * from "./util/BookingIntegrationUtils.js";
|
|
20
|
+
export * from "./util/IceServerUtils.js";
|
|
21
|
+
export * from "./util/PublicUrlUtils.js";
|
|
22
|
+
export * from "./util/RouteAccessUtils.js";
|
|
23
|
+
export * from "./util/TokenUtils.js";
|
|
24
|
+
export * from "./routes/BaseVideoMeetingRoute.js";
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E;;;;;;;;;;;;GAYG;AACH,cAAc,mBAAmB,CAAC;AAClC,cAAc,mCAAmC,CAAC;AAClD,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC,cAAc,mCAAmC,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
12
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14
|
+
import { ObjectDecorators } from "@rapidrest/core";
|
|
15
|
+
import { BaseMongoEntity, DocDecorators, ModelDecorators, PersistenceDecorators } from "@rapidrest/service-core";
|
|
16
|
+
import { MailboxScopedData } from "@rapidmx/restapi";
|
|
17
|
+
const { Description } = DocDecorators;
|
|
18
|
+
const { DataStore, Protect } = ModelDecorators;
|
|
19
|
+
const { Column, Entity, Index } = PersistenceDecorators;
|
|
20
|
+
const { Nullable } = ObjectDecorators;
|
|
21
|
+
/**
|
|
22
|
+
* Implementation of the `VideoMeetingInvitee` interface for storage in a MongoDB database. If SQL is desired,
|
|
23
|
+
* please use `models.sql.VideoMeetingInviteeSQL` instead.
|
|
24
|
+
*
|
|
25
|
+
* @author Jean-Philippe Steinmetz
|
|
26
|
+
*/
|
|
27
|
+
let VideoMeetingInviteeMongo = class VideoMeetingInviteeMongo extends BaseMongoEntity {
|
|
28
|
+
constructor(other) {
|
|
29
|
+
super(other);
|
|
30
|
+
this.meetingUid = "";
|
|
31
|
+
this.mailboxUid = "";
|
|
32
|
+
this.email = "";
|
|
33
|
+
this.joinToken = "";
|
|
34
|
+
if (other) {
|
|
35
|
+
this.meetingUid = other.meetingUid !== undefined ? other.meetingUid : this.meetingUid;
|
|
36
|
+
this.mailboxUid = other.mailboxUid !== undefined ? other.mailboxUid : this.mailboxUid;
|
|
37
|
+
this.email = other.email !== undefined ? other.email : this.email;
|
|
38
|
+
this.displayName = "displayName" in other ? other.displayName : this.displayName;
|
|
39
|
+
this.joinToken = other.joinToken !== undefined ? other.joinToken : this.joinToken;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
__decorate([
|
|
44
|
+
Column(),
|
|
45
|
+
Description("The unique identifier of the `VideoMeeting` this invitee belongs to."),
|
|
46
|
+
__metadata("design:type", String)
|
|
47
|
+
], VideoMeetingInviteeMongo.prototype, "meetingUid", void 0);
|
|
48
|
+
__decorate([
|
|
49
|
+
Column(),
|
|
50
|
+
Description("The unique identifier of the host `Mailbox`, denormalized from the meeting."),
|
|
51
|
+
__metadata("design:type", String)
|
|
52
|
+
], VideoMeetingInviteeMongo.prototype, "mailboxUid", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
Column(),
|
|
55
|
+
Description("The invitee's email address, normalized to lowercase."),
|
|
56
|
+
__metadata("design:type", String)
|
|
57
|
+
], VideoMeetingInviteeMongo.prototype, "email", void 0);
|
|
58
|
+
__decorate([
|
|
59
|
+
Column({ nullable: true }),
|
|
60
|
+
Description("The invitee's display name, if supplied."),
|
|
61
|
+
Nullable,
|
|
62
|
+
__metadata("design:type", String)
|
|
63
|
+
], VideoMeetingInviteeMongo.prototype, "displayName", void 0);
|
|
64
|
+
__decorate([
|
|
65
|
+
Column(),
|
|
66
|
+
Description("The unguessable token embedded in this invitee's join link. Minted server-side, immutable."),
|
|
67
|
+
__metadata("design:type", String)
|
|
68
|
+
], VideoMeetingInviteeMongo.prototype, "joinToken", void 0);
|
|
69
|
+
VideoMeetingInviteeMongo = __decorate([
|
|
70
|
+
DataStore("mongo"),
|
|
71
|
+
Entity(),
|
|
72
|
+
MailboxScopedData(),
|
|
73
|
+
Description("One invited participant of a PRIVATE VideoMeeting, identified by an unguessable join token."),
|
|
74
|
+
Index("videomeetinginvitee_join_token", ["joinToken"], { unique: true }),
|
|
75
|
+
Index("videomeetinginvitee_meeting", ["meetingUid"]),
|
|
76
|
+
Index("videomeetinginvitee_mailbox", ["mailboxUid"]),
|
|
77
|
+
Protect({
|
|
78
|
+
uid: "VideoMeetingInvitee",
|
|
79
|
+
records: [
|
|
80
|
+
{ userOrRoleId: "anonymous", actions: [] },
|
|
81
|
+
{ userOrRoleId: ".*", actions: [] },
|
|
82
|
+
],
|
|
83
|
+
}, false),
|
|
84
|
+
__metadata("design:paramtypes", [Object])
|
|
85
|
+
], VideoMeetingInviteeMongo);
|
|
86
|
+
export { VideoMeetingInviteeMongo };
|
|
87
|
+
//# sourceMappingURL=VideoMeetingInviteeMongo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoMeetingInviteeMongo.js","sourceRoot":"","sources":["../../../../src/models/mongo/VideoMeetingInviteeMongo.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
12
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14
|
+
import { ObjectDecorators } from "@rapidrest/core";
|
|
15
|
+
import { BaseMongoEntity, DocDecorators, ModelDecorators, PersistenceDecorators } from "@rapidrest/service-core";
|
|
16
|
+
import { MailboxScopedData } from "@rapidmx/restapi";
|
|
17
|
+
import { VideoMeetingStatus, VideoMeetingVisibility } from "../types.js";
|
|
18
|
+
const { Description } = DocDecorators;
|
|
19
|
+
const { DataStore, Protect } = ModelDecorators;
|
|
20
|
+
const { Column, Entity, Index } = PersistenceDecorators;
|
|
21
|
+
const { Nullable } = ObjectDecorators;
|
|
22
|
+
/**
|
|
23
|
+
* Implementation of the `VideoMeeting` interface for storage in a MongoDB database. If SQL is desired, please use
|
|
24
|
+
* `models.sql.VideoMeetingSQL` instead.
|
|
25
|
+
*
|
|
26
|
+
* `recordACL: true` - see the `VideoMeeting` interface's own doc comment for why this entity, unlike most of this
|
|
27
|
+
* codebase's mailbox-scoped data, gets its own per-record `AccessControlList`.
|
|
28
|
+
*
|
|
29
|
+
* @author Jean-Philippe Steinmetz
|
|
30
|
+
*/
|
|
31
|
+
let VideoMeetingMongo = class VideoMeetingMongo extends BaseMongoEntity {
|
|
32
|
+
constructor(other) {
|
|
33
|
+
super(other);
|
|
34
|
+
this.mailboxUid = "";
|
|
35
|
+
this.title = "";
|
|
36
|
+
this.visibility = VideoMeetingVisibility.PRIVATE;
|
|
37
|
+
this.status = VideoMeetingStatus.SCHEDULED;
|
|
38
|
+
if (other) {
|
|
39
|
+
this.mailboxUid = other.mailboxUid !== undefined ? other.mailboxUid : this.mailboxUid;
|
|
40
|
+
this.calendarEventUid = "calendarEventUid" in other ? other.calendarEventUid : this.calendarEventUid;
|
|
41
|
+
this.title = other.title !== undefined ? other.title : this.title;
|
|
42
|
+
this.visibility = other.visibility !== undefined ? other.visibility : this.visibility;
|
|
43
|
+
this.publicSlug = "publicSlug" in other ? other.publicSlug : this.publicSlug;
|
|
44
|
+
this.organizerSlug = "organizerSlug" in other ? other.organizerSlug : this.organizerSlug;
|
|
45
|
+
this.status = other.status !== undefined ? other.status : this.status;
|
|
46
|
+
this.startTime = "startTime" in other ? other.startTime : this.startTime;
|
|
47
|
+
this.endTime = "endTime" in other ? other.endTime : this.endTime;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
__decorate([
|
|
52
|
+
Column(),
|
|
53
|
+
Description("The unique identifier of the `Mailbox` that owns this meeting."),
|
|
54
|
+
__metadata("design:type", String)
|
|
55
|
+
], VideoMeetingMongo.prototype, "mailboxUid", void 0);
|
|
56
|
+
__decorate([
|
|
57
|
+
Column({ nullable: true }),
|
|
58
|
+
Description("The unique identifier of the `CalendarEvent` this meeting was minted for, if any."),
|
|
59
|
+
Nullable,
|
|
60
|
+
__metadata("design:type", String)
|
|
61
|
+
], VideoMeetingMongo.prototype, "calendarEventUid", void 0);
|
|
62
|
+
__decorate([
|
|
63
|
+
Column(),
|
|
64
|
+
Description("The meeting's display title."),
|
|
65
|
+
__metadata("design:type", String)
|
|
66
|
+
], VideoMeetingMongo.prototype, "title", void 0);
|
|
67
|
+
__decorate([
|
|
68
|
+
Column(),
|
|
69
|
+
Description("Whether this meeting is joinable only by its invitees or by anyone holding its public link."),
|
|
70
|
+
__metadata("design:type", String)
|
|
71
|
+
], VideoMeetingMongo.prototype, "visibility", void 0);
|
|
72
|
+
__decorate([
|
|
73
|
+
Column({ nullable: true }),
|
|
74
|
+
Description("The public join link's unique identifier, set only when visibility is PUBLIC."),
|
|
75
|
+
Nullable,
|
|
76
|
+
__metadata("design:type", String)
|
|
77
|
+
], VideoMeetingMongo.prototype, "publicSlug", void 0);
|
|
78
|
+
__decorate([
|
|
79
|
+
Column({ nullable: true }),
|
|
80
|
+
Description("The organizer's own join link identifier, set only when visibility is PRIVATE."),
|
|
81
|
+
Nullable,
|
|
82
|
+
__metadata("design:type", String)
|
|
83
|
+
], VideoMeetingMongo.prototype, "organizerSlug", void 0);
|
|
84
|
+
__decorate([
|
|
85
|
+
Column(),
|
|
86
|
+
Description("The current lifecycle state of this meeting."),
|
|
87
|
+
__metadata("design:type", String)
|
|
88
|
+
], VideoMeetingMongo.prototype, "status", void 0);
|
|
89
|
+
__decorate([
|
|
90
|
+
Column({ nullable: true }),
|
|
91
|
+
Description("Informational only: not used for any availability or conflict checking."),
|
|
92
|
+
Nullable,
|
|
93
|
+
__metadata("design:type", Date)
|
|
94
|
+
], VideoMeetingMongo.prototype, "startTime", void 0);
|
|
95
|
+
__decorate([
|
|
96
|
+
Column({ nullable: true }),
|
|
97
|
+
Description("Informational only: not used for any availability or conflict checking."),
|
|
98
|
+
Nullable,
|
|
99
|
+
__metadata("design:type", Date)
|
|
100
|
+
], VideoMeetingMongo.prototype, "endTime", void 0);
|
|
101
|
+
VideoMeetingMongo = __decorate([
|
|
102
|
+
DataStore("mongo"),
|
|
103
|
+
Entity(),
|
|
104
|
+
MailboxScopedData(),
|
|
105
|
+
Description("A WebRTC video meeting, joinable directly or from a calendar invite."),
|
|
106
|
+
Index("videomeeting_mailbox", ["mailboxUid"])
|
|
107
|
+
// Single-field, not `["mailboxUid", "publicSlug"]`: a compound sparse index still indexes a document carrying at
|
|
108
|
+
// least one of its keys, so every document has `mailboxUid` and would be indexed regardless of whether `publicSlug`
|
|
109
|
+
// is set - two private meetings in the same mailbox (both missing `publicSlug`) would then collide on `(mailboxUid,
|
|
110
|
+
// null)`, rejecting the second one outright. A single-field sparse index skips a document missing the field
|
|
111
|
+
// entirely, matching the global (not per-mailbox) lookup `join()` actually performs - see `VideoMeeting.publicSlug`'s
|
|
112
|
+
// own doc comment on that tradeoff. `organizerSlug` below hit the exact same pitfall and is fixed the same way.
|
|
113
|
+
,
|
|
114
|
+
Index("videomeeting_public_slug", ["publicSlug"], { unique: true, sparse: true }),
|
|
115
|
+
Index("videomeeting_organizer_slug", ["organizerSlug"], { unique: true, sparse: true }),
|
|
116
|
+
Protect({
|
|
117
|
+
uid: "VideoMeeting",
|
|
118
|
+
records: [
|
|
119
|
+
{ userOrRoleId: "anonymous", actions: [] },
|
|
120
|
+
{ userOrRoleId: ".*", actions: [] },
|
|
121
|
+
],
|
|
122
|
+
}, true),
|
|
123
|
+
__metadata("design:paramtypes", [Object])
|
|
124
|
+
], VideoMeetingMongo);
|
|
125
|
+
export { VideoMeetingMongo };
|
|
126
|
+
//# sourceMappingURL=VideoMeetingMongo.js.map
|