@rapidmx/meet-plugin 0.3.0 → 0.3.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/apps/meet/_CallView.tsx +5 -3
- package/apps/shared/webrtc/MeshConnectionManager.ts +26 -12
- package/apps/shared/webrtc/types.ts +9 -3
- package/dist/apps/meet/_CallView.d.ts +3 -2
- package/dist/apps/meet/_CallView.js +5 -3
- package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +6 -1
- package/dist/apps/shared/webrtc/MeshConnectionManager.js +21 -12
- package/dist/apps/shared/webrtc/types.d.ts +9 -3
- package/package.json +1 -1
package/apps/meet/_CallView.tsx
CHANGED
|
@@ -93,8 +93,9 @@ export function computeMainUid(options: {
|
|
|
93
93
|
return options.otherUids[0];
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** This tab's identity
|
|
97
|
-
*
|
|
96
|
+
/** This tab's identity in the call: the account (or guest) uid plus a random suffix, so the same account joining
|
|
97
|
+
* from two devices - or two tabs - is two participants rather than one that ignores itself. It travels as the
|
|
98
|
+
* `peer` of each message; `from` stays the exact authenticated uid, which the server requires. */
|
|
98
99
|
export function newPeerId(selfUid: string): string {
|
|
99
100
|
return `${selfUid}~${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
100
101
|
}
|
|
@@ -137,7 +138,8 @@ export default function CallView({ channel, token, selfUid, selfName, meetingTit
|
|
|
137
138
|
let cancelled = false;
|
|
138
139
|
const client = new GuestSignalingClient({ channel, token });
|
|
139
140
|
const manager = new MeshConnectionManager({
|
|
140
|
-
selfUid
|
|
141
|
+
selfUid,
|
|
142
|
+
peerId,
|
|
141
143
|
selfName,
|
|
142
144
|
iceServers,
|
|
143
145
|
channel: client,
|
|
@@ -75,8 +75,8 @@ import {
|
|
|
75
75
|
} from "./types.js";
|
|
76
76
|
|
|
77
77
|
/** The lexicographically smaller uid is always the offerer for that pair - see this module's doc comment. */
|
|
78
|
-
export function isOfferer(
|
|
79
|
-
return
|
|
78
|
+
export function isOfferer(selfId: string, peerId: string): boolean {
|
|
79
|
+
return selfId < peerId;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** How many peers' worth of early ICE candidates are held, and how many candidates per peer - bounds the memory a
|
|
@@ -96,7 +96,11 @@ interface PeerState extends MeshParticipant {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export interface MeshConnectionManagerOptions {
|
|
99
|
+
/** The authenticated uid - what `from` must be on the wire (see `SignalMessage.from`). */
|
|
99
100
|
selfUid: string;
|
|
101
|
+
/** This tab's identity in the call, `<selfUid>~<random>` - defaults to `selfUid`. Everything the manager keys on
|
|
102
|
+
* (the roster, who offers, who presents) is a peer id, so two tabs of one account are two participants. */
|
|
103
|
+
peerId?: string;
|
|
100
104
|
selfName: string;
|
|
101
105
|
iceServers: RTCIceServer[];
|
|
102
106
|
channel: SignalingChannel;
|
|
@@ -122,8 +126,10 @@ export class MeshConnectionManager {
|
|
|
122
126
|
private started = false;
|
|
123
127
|
private stopped = false;
|
|
124
128
|
private currentPresenterUid: string | undefined;
|
|
129
|
+
private readonly selfId: string;
|
|
125
130
|
|
|
126
131
|
constructor(private readonly options: MeshConnectionManagerOptions) {
|
|
132
|
+
this.selfId = options.peerId ?? options.selfUid;
|
|
127
133
|
this.localTracks = { audio: options.localAudioTrack ?? null, video: options.localVideoTrack ?? null };
|
|
128
134
|
this.localState = {
|
|
129
135
|
audioOn: !!options.localAudioTrack,
|
|
@@ -213,10 +219,10 @@ export class MeshConnectionManager {
|
|
|
213
219
|
* else already presents - the caller (`_CallView.tsx`) uses this to disable its own "share screen" control
|
|
214
220
|
* rather than let a claim silently do nothing. */
|
|
215
221
|
claimPresenter(): boolean {
|
|
216
|
-
if (this.currentPresenterUid !== undefined && this.currentPresenterUid !== this.
|
|
222
|
+
if (this.currentPresenterUid !== undefined && this.currentPresenterUid !== this.selfId) {
|
|
217
223
|
return false;
|
|
218
224
|
}
|
|
219
|
-
this.currentPresenterUid = this.
|
|
225
|
+
this.currentPresenterUid = this.selfId;
|
|
220
226
|
this.send({ kind: "presenter-claim" });
|
|
221
227
|
this.emit({ type: "presenter-changed", uid: this.currentPresenterUid });
|
|
222
228
|
return true;
|
|
@@ -224,7 +230,7 @@ export class MeshConnectionManager {
|
|
|
224
230
|
|
|
225
231
|
/** Releases presenter status - a no-op unless the local participant currently holds it. */
|
|
226
232
|
releasePresenter(): void {
|
|
227
|
-
if (this.currentPresenterUid !== this.
|
|
233
|
+
if (this.currentPresenterUid !== this.selfId) {
|
|
228
234
|
return;
|
|
229
235
|
}
|
|
230
236
|
this.currentPresenterUid = undefined;
|
|
@@ -237,7 +243,7 @@ export class MeshConnectionManager {
|
|
|
237
243
|
}
|
|
238
244
|
|
|
239
245
|
private send(partial: Omit<SignalMessage, "type" | "from">): void {
|
|
240
|
-
this.options.channel.send({ type: "video-meeting-signal", from: this.options.selfUid, ...partial });
|
|
246
|
+
this.options.channel.send({ type: "video-meeting-signal", from: this.options.selfUid, peer: this.selfId, ...partial });
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
private emit(event: MeshEvent): void {
|
|
@@ -246,11 +252,19 @@ export class MeshConnectionManager {
|
|
|
246
252
|
}
|
|
247
253
|
}
|
|
248
254
|
|
|
249
|
-
private handleMessage(
|
|
250
|
-
if (
|
|
255
|
+
private handleMessage(raw: SignalMessage): void {
|
|
256
|
+
if (raw.type !== "video-meeting-signal") {
|
|
251
257
|
return;
|
|
252
258
|
}
|
|
253
|
-
|
|
259
|
+
// From here on `from` is the sender's peer id - see `SignalMessage.peer`.
|
|
260
|
+
if (raw.peer !== undefined && raw.peer !== raw.from && !raw.peer.startsWith(`${raw.from}~`)) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const message: SignalMessage = { ...raw, from: raw.peer ?? raw.from };
|
|
264
|
+
if (message.from === this.selfId) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (message.to !== undefined && message.to !== this.selfId) {
|
|
254
268
|
return;
|
|
255
269
|
}
|
|
256
270
|
switch (message.kind) {
|
|
@@ -291,7 +305,7 @@ export class MeshConnectionManager {
|
|
|
291
305
|
this.updatePeer(known, message.name, message.state);
|
|
292
306
|
return;
|
|
293
307
|
}
|
|
294
|
-
const offerer = isOfferer(this.
|
|
308
|
+
const offerer = isOfferer(this.selfId, message.from);
|
|
295
309
|
const peer = this.createPeer(message.from, message.name ?? message.from, message.state, offerer);
|
|
296
310
|
this.emit({ type: "participant-joined", participant: toParticipant(peer) });
|
|
297
311
|
// Let a newcomer who couldn't have seen our own original `hello` learn about us too - see this module's
|
|
@@ -481,12 +495,12 @@ export class MeshConnectionManager {
|
|
|
481
495
|
// Collision: a claim from someone other than the presenter we already recorded. Resolve deterministically
|
|
482
496
|
// - see this module's doc comment - so every participant converges on the same winner. When the
|
|
483
497
|
// already-recorded presenter has the smaller uid, it wins and nothing changes here - including when that
|
|
484
|
-
// presenter is
|
|
498
|
+
// presenter is this participant itself, whose own optimistic claim simply stays in place.
|
|
485
499
|
if (from < this.currentPresenterUid) {
|
|
486
500
|
const loser = this.currentPresenterUid;
|
|
487
501
|
this.currentPresenterUid = from;
|
|
488
502
|
this.emit({ type: "presenter-changed", uid: from });
|
|
489
|
-
if (loser === this.
|
|
503
|
+
if (loser === this.selfId) {
|
|
490
504
|
this.send({ kind: "presenter-release" });
|
|
491
505
|
}
|
|
492
506
|
}
|
|
@@ -45,10 +45,16 @@ export interface SignalMessage {
|
|
|
45
45
|
| "presenter-release"
|
|
46
46
|
| "state"
|
|
47
47
|
| "reaction";
|
|
48
|
-
/** The sender's
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
/** The sender's authenticated uid - the real caller's own uid when `join()` returned `authenticated: true`, else
|
|
49
|
+
* the `guest:<random>` uid `BaseVideoMeetingRoute.join()` minted. The server refuses a published message whose
|
|
50
|
+
* `from` isn't the authenticated caller's uid (it stops one participant speaking as another), so this must be
|
|
51
|
+
* exactly that uid. */
|
|
51
52
|
from: string;
|
|
53
|
+
/** Which of the sender's tabs or devices this is: `<from>~<random>`. The same account joining from two devices is
|
|
54
|
+
* two participants, not one that ignores its own messages. A `peer` that doesn't start with `<from>~` is
|
|
55
|
+
* ignored, so it can't name someone else's tab. Absent from a sender that predates it (the participant is then
|
|
56
|
+
* identified by `from`). */
|
|
57
|
+
peer?: string;
|
|
52
58
|
/** Set only on a point-to-point message (`offer`/`answer`/`ice-candidate`). */
|
|
53
59
|
to?: string;
|
|
54
60
|
/** `hello` only - the display name the sender chose in the lobby. */
|
|
@@ -54,7 +54,8 @@ export declare function computeMainUid(options: {
|
|
|
54
54
|
activeSpeakerUid?: string;
|
|
55
55
|
otherUids: readonly string[];
|
|
56
56
|
}): string | undefined;
|
|
57
|
-
/** This tab's identity
|
|
58
|
-
*
|
|
57
|
+
/** This tab's identity in the call: the account (or guest) uid plus a random suffix, so the same account joining
|
|
58
|
+
* from two devices - or two tabs - is two participants rather than one that ignores itself. It travels as the
|
|
59
|
+
* `peer` of each message; `from` stays the exact authenticated uid, which the server requires. */
|
|
59
60
|
export declare function newPeerId(selfUid: string): string;
|
|
60
61
|
export default function CallView({ channel, token, selfUid, selfName, meetingTitle, iceServers, media, onLeave }: CallViewProps): React.JSX.Element;
|
|
@@ -63,8 +63,9 @@ export function computeMainUid(options) {
|
|
|
63
63
|
}
|
|
64
64
|
return options.otherUids[0];
|
|
65
65
|
}
|
|
66
|
-
/** This tab's identity
|
|
67
|
-
*
|
|
66
|
+
/** This tab's identity in the call: the account (or guest) uid plus a random suffix, so the same account joining
|
|
67
|
+
* from two devices - or two tabs - is two participants rather than one that ignores itself. It travels as the
|
|
68
|
+
* `peer` of each message; `from` stays the exact authenticated uid, which the server requires. */
|
|
68
69
|
export function newPeerId(selfUid) {
|
|
69
70
|
return `${selfUid}~${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
70
71
|
}
|
|
@@ -103,7 +104,8 @@ export default function CallView({ channel, token, selfUid, selfName, meetingTit
|
|
|
103
104
|
let cancelled = false;
|
|
104
105
|
const client = new GuestSignalingClient({ channel, token });
|
|
105
106
|
const manager = new MeshConnectionManager({
|
|
106
|
-
selfUid
|
|
107
|
+
selfUid,
|
|
108
|
+
peerId,
|
|
107
109
|
selfName,
|
|
108
110
|
iceServers,
|
|
109
111
|
channel: client,
|
|
@@ -60,10 +60,14 @@
|
|
|
60
60
|
*/
|
|
61
61
|
import { type MeshEvent, type MeshParticipant, type ParticipantState, type RTCPeerConnectionFactory, type SignalingChannel } from "./types.js";
|
|
62
62
|
/** The lexicographically smaller uid is always the offerer for that pair - see this module's doc comment. */
|
|
63
|
-
export declare function isOfferer(
|
|
63
|
+
export declare function isOfferer(selfId: string, peerId: string): boolean;
|
|
64
64
|
type MediaKind = "audio" | "video";
|
|
65
65
|
export interface MeshConnectionManagerOptions {
|
|
66
|
+
/** The authenticated uid - what `from` must be on the wire (see `SignalMessage.from`). */
|
|
66
67
|
selfUid: string;
|
|
68
|
+
/** This tab's identity in the call, `<selfUid>~<random>` - defaults to `selfUid`. Everything the manager keys on
|
|
69
|
+
* (the roster, who offers, who presents) is a peer id, so two tabs of one account are two participants. */
|
|
70
|
+
peerId?: string;
|
|
67
71
|
selfName: string;
|
|
68
72
|
iceServers: RTCIceServer[];
|
|
69
73
|
channel: SignalingChannel;
|
|
@@ -89,6 +93,7 @@ export declare class MeshConnectionManager {
|
|
|
89
93
|
private started;
|
|
90
94
|
private stopped;
|
|
91
95
|
private currentPresenterUid;
|
|
96
|
+
private readonly selfId;
|
|
92
97
|
constructor(options: MeshConnectionManagerOptions);
|
|
93
98
|
get participants(): MeshParticipant[];
|
|
94
99
|
get presenterUid(): string | undefined;
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
*/
|
|
65
65
|
import { REACTION_EMOJIS, } from "./types.js";
|
|
66
66
|
/** The lexicographically smaller uid is always the offerer for that pair - see this module's doc comment. */
|
|
67
|
-
export function isOfferer(
|
|
68
|
-
return
|
|
67
|
+
export function isOfferer(selfId, peerId) {
|
|
68
|
+
return selfId < peerId;
|
|
69
69
|
}
|
|
70
70
|
/** How many peers' worth of early ICE candidates are held, and how many candidates per peer - bounds the memory a
|
|
71
71
|
* misbehaving participant could make this hold for peers that never materialize. */
|
|
@@ -79,6 +79,7 @@ export class MeshConnectionManager {
|
|
|
79
79
|
this.orphanCandidates = new Map();
|
|
80
80
|
this.started = false;
|
|
81
81
|
this.stopped = false;
|
|
82
|
+
this.selfId = options.peerId ?? options.selfUid;
|
|
82
83
|
this.localTracks = { audio: options.localAudioTrack ?? null, video: options.localVideoTrack ?? null };
|
|
83
84
|
this.localState = {
|
|
84
85
|
audioOn: !!options.localAudioTrack,
|
|
@@ -159,17 +160,17 @@ export class MeshConnectionManager {
|
|
|
159
160
|
* else already presents - the caller (`_CallView.tsx`) uses this to disable its own "share screen" control
|
|
160
161
|
* rather than let a claim silently do nothing. */
|
|
161
162
|
claimPresenter() {
|
|
162
|
-
if (this.currentPresenterUid !== undefined && this.currentPresenterUid !== this.
|
|
163
|
+
if (this.currentPresenterUid !== undefined && this.currentPresenterUid !== this.selfId) {
|
|
163
164
|
return false;
|
|
164
165
|
}
|
|
165
|
-
this.currentPresenterUid = this.
|
|
166
|
+
this.currentPresenterUid = this.selfId;
|
|
166
167
|
this.send({ kind: "presenter-claim" });
|
|
167
168
|
this.emit({ type: "presenter-changed", uid: this.currentPresenterUid });
|
|
168
169
|
return true;
|
|
169
170
|
}
|
|
170
171
|
/** Releases presenter status - a no-op unless the local participant currently holds it. */
|
|
171
172
|
releasePresenter() {
|
|
172
|
-
if (this.currentPresenterUid !== this.
|
|
173
|
+
if (this.currentPresenterUid !== this.selfId) {
|
|
173
174
|
return;
|
|
174
175
|
}
|
|
175
176
|
this.currentPresenterUid = undefined;
|
|
@@ -180,18 +181,26 @@ export class MeshConnectionManager {
|
|
|
180
181
|
this.send({ kind: "hello", name: this.options.selfName, state: this.localState });
|
|
181
182
|
}
|
|
182
183
|
send(partial) {
|
|
183
|
-
this.options.channel.send({ type: "video-meeting-signal", from: this.options.selfUid, ...partial });
|
|
184
|
+
this.options.channel.send({ type: "video-meeting-signal", from: this.options.selfUid, peer: this.selfId, ...partial });
|
|
184
185
|
}
|
|
185
186
|
emit(event) {
|
|
186
187
|
for (const listener of [...this.listeners]) {
|
|
187
188
|
listener(event);
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
|
-
handleMessage(
|
|
191
|
-
if (
|
|
191
|
+
handleMessage(raw) {
|
|
192
|
+
if (raw.type !== "video-meeting-signal") {
|
|
192
193
|
return;
|
|
193
194
|
}
|
|
194
|
-
|
|
195
|
+
// From here on `from` is the sender's peer id - see `SignalMessage.peer`.
|
|
196
|
+
if (raw.peer !== undefined && raw.peer !== raw.from && !raw.peer.startsWith(`${raw.from}~`)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const message = { ...raw, from: raw.peer ?? raw.from };
|
|
200
|
+
if (message.from === this.selfId) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (message.to !== undefined && message.to !== this.selfId) {
|
|
195
204
|
return;
|
|
196
205
|
}
|
|
197
206
|
switch (message.kind) {
|
|
@@ -231,7 +240,7 @@ export class MeshConnectionManager {
|
|
|
231
240
|
this.updatePeer(known, message.name, message.state);
|
|
232
241
|
return;
|
|
233
242
|
}
|
|
234
|
-
const offerer = isOfferer(this.
|
|
243
|
+
const offerer = isOfferer(this.selfId, message.from);
|
|
235
244
|
const peer = this.createPeer(message.from, message.name ?? message.from, message.state, offerer);
|
|
236
245
|
this.emit({ type: "participant-joined", participant: toParticipant(peer) });
|
|
237
246
|
// Let a newcomer who couldn't have seen our own original `hello` learn about us too - see this module's
|
|
@@ -411,12 +420,12 @@ export class MeshConnectionManager {
|
|
|
411
420
|
// Collision: a claim from someone other than the presenter we already recorded. Resolve deterministically
|
|
412
421
|
// - see this module's doc comment - so every participant converges on the same winner. When the
|
|
413
422
|
// already-recorded presenter has the smaller uid, it wins and nothing changes here - including when that
|
|
414
|
-
// presenter is
|
|
423
|
+
// presenter is this participant itself, whose own optimistic claim simply stays in place.
|
|
415
424
|
if (from < this.currentPresenterUid) {
|
|
416
425
|
const loser = this.currentPresenterUid;
|
|
417
426
|
this.currentPresenterUid = from;
|
|
418
427
|
this.emit({ type: "presenter-changed", uid: from });
|
|
419
|
-
if (loser === this.
|
|
428
|
+
if (loser === this.selfId) {
|
|
420
429
|
this.send({ kind: "presenter-release" });
|
|
421
430
|
}
|
|
422
431
|
}
|
|
@@ -29,10 +29,16 @@ export declare const REACTION_EMOJIS: readonly ["👍", "👏", "❤️", "🎉"
|
|
|
29
29
|
export interface SignalMessage {
|
|
30
30
|
type: "video-meeting-signal";
|
|
31
31
|
kind: "hello" | "bye" | "offer" | "answer" | "ice-candidate" | "presenter-claim" | "presenter-release" | "state" | "reaction";
|
|
32
|
-
/** The sender's
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
/** The sender's authenticated uid - the real caller's own uid when `join()` returned `authenticated: true`, else
|
|
33
|
+
* the `guest:<random>` uid `BaseVideoMeetingRoute.join()` minted. The server refuses a published message whose
|
|
34
|
+
* `from` isn't the authenticated caller's uid (it stops one participant speaking as another), so this must be
|
|
35
|
+
* exactly that uid. */
|
|
35
36
|
from: string;
|
|
37
|
+
/** Which of the sender's tabs or devices this is: `<from>~<random>`. The same account joining from two devices is
|
|
38
|
+
* two participants, not one that ignores its own messages. A `peer` that doesn't start with `<from>~` is
|
|
39
|
+
* ignored, so it can't name someone else's tab. Absent from a sender that predates it (the participant is then
|
|
40
|
+
* identified by `from`). */
|
|
41
|
+
peer?: string;
|
|
36
42
|
/** Set only on a point-to-point message (`offer`/`answer`/`ice-candidate`). */
|
|
37
43
|
to?: string;
|
|
38
44
|
/** `hello` only - the display name the sender chose in the lobby. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidmx/meet-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "RapidMX plugin that provides WebRTC video conferencing, joinable from a calendar invite or a shared link.",
|
|
5
5
|
"repository": "https://github.com/RapidMX/meet-plugin.git",
|
|
6
6
|
"author": "RapidREST <rapidrests@gmail.com>",
|