@rapidmx/meet-plugin 0.3.0 → 0.4.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.
@@ -93,8 +93,9 @@ export function computeMainUid(options: {
93
93
  return options.otherUids[0];
94
94
  }
95
95
 
96
- /** This tab's identity on the signaling channel: the account (or guest) uid plus a random suffix, so the same
97
- * account joining from two devices - or two tabs - is two participants rather than one that ignores itself. */
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: peerId,
141
+ selfUid,
142
+ peerId,
141
143
  selfName,
142
144
  iceServers,
143
145
  channel: client,
@@ -17,7 +17,7 @@ export type VideoMeetingStatus = "scheduled" | "active" | "ended" | "cancelled";
17
17
  /** Mirrors `BaseVideoMeetingRoute.ts`'s `IceServerConfig` - already the exact shape `RTCPeerConnection`'s
18
18
  * `iceServers` constructor option expects, passed straight through with no reshaping. */
19
19
  export interface IceServerConfig {
20
- urls: string;
20
+ urls: string | string[];
21
21
  username?: string;
22
22
  credential?: string;
23
23
  }
@@ -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(selfUid: string, peerUid: string): boolean {
79
- return selfUid < peerUid;
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.options.selfUid) {
222
+ if (this.currentPresenterUid !== undefined && this.currentPresenterUid !== this.selfId) {
217
223
  return false;
218
224
  }
219
- this.currentPresenterUid = this.options.selfUid;
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.options.selfUid) {
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(message: SignalMessage): void {
250
- if (message.type !== "video-meeting-signal" || message.from === this.options.selfUid) {
255
+ private handleMessage(raw: SignalMessage): void {
256
+ if (raw.type !== "video-meeting-signal") {
251
257
  return;
252
258
  }
253
- if (message.to !== undefined && message.to !== this.options.selfUid) {
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.options.selfUid, message.from);
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 `this.options.selfUid` itself, whose own optimistic claim simply stays in place.
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.options.selfUid) {
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 per-call peer id - `CallView` derives it from `VideoMeetingJoinResult.selfUid` plus a random
49
- * suffix, so the same account joining from two devices (or two tabs) is two participants, not one that ignores
50
- * its own messages. */
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 on the signaling channel: the account (or guest) uid plus a random suffix, so the same
58
- * account joining from two devices - or two tabs - is two participants rather than one that ignores itself. */
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 on the signaling channel: the account (or guest) uid plus a random suffix, so the same
67
- * account joining from two devices - or two tabs - is two participants rather than one that ignores itself. */
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: peerId,
107
+ selfUid,
108
+ peerId,
107
109
  selfName,
108
110
  iceServers,
109
111
  channel: client,
@@ -3,7 +3,7 @@ export type VideoMeetingStatus = "scheduled" | "active" | "ended" | "cancelled";
3
3
  /** Mirrors `BaseVideoMeetingRoute.ts`'s `IceServerConfig` - already the exact shape `RTCPeerConnection`'s
4
4
  * `iceServers` constructor option expects, passed straight through with no reshaping. */
5
5
  export interface IceServerConfig {
6
- urls: string;
6
+ urls: string | string[];
7
7
  username?: string;
8
8
  credential?: string;
9
9
  }
@@ -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(selfUid: string, peerUid: string): boolean;
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(selfUid, peerUid) {
68
- return selfUid < peerUid;
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.options.selfUid) {
163
+ if (this.currentPresenterUid !== undefined && this.currentPresenterUid !== this.selfId) {
163
164
  return false;
164
165
  }
165
- this.currentPresenterUid = this.options.selfUid;
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.options.selfUid) {
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(message) {
191
- if (message.type !== "video-meeting-signal" || message.from === this.options.selfUid) {
191
+ handleMessage(raw) {
192
+ if (raw.type !== "video-meeting-signal") {
192
193
  return;
193
194
  }
194
- if (message.to !== undefined && message.to !== this.options.selfUid) {
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.options.selfUid, message.from);
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 `this.options.selfUid` itself, whose own optimistic claim simply stays in place.
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.options.selfUid) {
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 per-call peer id - `CallView` derives it from `VideoMeetingJoinResult.selfUid` plus a random
33
- * suffix, so the same account joining from two devices (or two tabs) is two participants, not one that ignores
34
- * its own messages. */
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. */
@@ -3,6 +3,11 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import * as crypto from "crypto";
6
+ /** The URLs in a `mail:videoconf:turn:url` value: one, or several separated by commas and/or whitespace (for a TURN server
7
+ * listening on both UDP and TLS, `turn:host:3478,turns:host:5349`). Blank entries are dropped. */
8
+ export function parseTurnUrls(value) {
9
+ return value.split(/[\s,]+/).filter(Boolean);
10
+ }
6
11
  /** Always-available, free public STUN servers - discovery only, no media relay. Two independent providers, so a
7
12
  * call still has STUN available if one of them is ever unreachable. */
8
13
  export const DEFAULT_STUN_SERVERS = [
@@ -48,10 +53,12 @@ export function turnRestCredential(sharedSecret, userPart, ttlSeconds = DEFAULT_
48
53
  */
49
54
  export function buildIceServers(settings, options) {
50
55
  const servers = [...DEFAULT_STUN_SERVERS];
51
- const url = settings.url.trim();
52
- if (!url) {
56
+ const turnUrls = parseTurnUrls(settings.url);
57
+ if (turnUrls.length === 0) {
53
58
  return servers;
54
59
  }
60
+ // A single URL stays a string, exactly as before, so a deployment that never sets more than one sees no change.
61
+ const url = turnUrls.length === 1 ? turnUrls[0] : turnUrls;
55
62
  if (settings.sharedSecret.trim()) {
56
63
  const userPart = settings.username.trim() || (options?.randomUserPart ?? (() => crypto.randomBytes(8).toString("hex")))();
57
64
  const { username, credential } = turnRestCredential(settings.sharedSecret.trim(), userPart, options?.ttlSeconds, options?.now);
@@ -1 +1 @@
1
- {"version":3,"file":"IceServerUtils.js","sourceRoot":"","sources":["../../../src/util/IceServerUtils.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAkBjC;uEACuE;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAA+B;IAC5D,EAAE,IAAI,EAAE,8BAA8B,EAAE;IACxC,EAAE,IAAI,EAAE,+BAA+B,EAAE;CAC5C,CAAC;AAEF;;iBAEiB;AACjB,MAAM,CAAC,MAAM,mCAAmC,GAAW,EAAE,GAAG,EAAE,CAAC;AAEnE;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAC9B,YAAoB,EACpB,QAAgB,EAChB,aAAqB,mCAAmC,EACxD,MAAY,IAAI,IAAI,EAAE;IAEtB,MAAM,MAAM,GAAW,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAC9F,MAAM,QAAQ,GAAW,GAAG,MAAM,IAAI,QAAQ,EAAE,CAAC;IACjD,MAAM,UAAU,GAAW,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrG,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AACpC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAC3B,QAAsB,EACtB,OAA4E;IAE5E,MAAM,OAAO,GAAsB,CAAC,GAAG,oBAAoB,CAAC,CAAC;IAC7D,MAAM,GAAG,GAAW,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IACxC,IAAI,CAAC,GAAG,EAAE,CAAC;QACP,OAAO,OAAO,CAAC;IACnB,CAAC;IACD,IAAI,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAW,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,cAAc,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAClI,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,kBAAkB,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;QAC/H,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC;IACtD,CAAC;SAAM,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;QAChE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC5G,CAAC;SAAM,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC"}
1
+ {"version":3,"file":"IceServerUtils.js","sourceRoot":"","sources":["../../../src/util/IceServerUtils.ts"],"names":[],"mappings":"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"}
@@ -1,17 +1,22 @@
1
1
  /** A single ICE server entry, matching the shape the browser `RTCPeerConnection` `iceServers` constructor option
2
2
  * expects (`RTCIceServer`), so the join response can be handed to it directly with no reshaping. */
3
3
  export interface IceServerConfig {
4
- urls: string;
4
+ /** One URL, or several that share one credential (a TURN server's UDP and TLS addresses, say). */
5
+ urls: string | string[];
5
6
  username?: string;
6
7
  credential?: string;
7
8
  }
8
- /** The `mail:videoconf:turn:*` settings, exactly as configured (empty strings for anything unset). */
9
+ /** The `mail:videoconf:turn:*` settings, exactly as configured (empty strings for anything unset). `url` may name several
10
+ * URLs, separated by commas and/or whitespace - see `parseTurnUrls()`. */
9
11
  export interface TurnSettings {
10
12
  url: string;
11
13
  username: string;
12
14
  credential: string;
13
15
  sharedSecret: string;
14
16
  }
17
+ /** The URLs in a `mail:videoconf:turn:url` value: one, or several separated by commas and/or whitespace (for a TURN server
18
+ * listening on both UDP and TLS, `turn:host:3478,turns:host:5349`). Blank entries are dropped. */
19
+ export declare function parseTurnUrls(value: string): string[];
15
20
  /** Always-available, free public STUN servers - discovery only, no media relay. Two independent providers, so a
16
21
  * call still has STUN available if one of them is ever unreachable. */
17
22
  export declare const DEFAULT_STUN_SERVERS: readonly IceServerConfig[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidmx/meet-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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>",
@@ -128,7 +128,7 @@
128
128
  "label": "TURN server URL",
129
129
  "type": "string",
130
130
  "default": "",
131
- "help": "A turn: or turns: URL (e.g. turns:turn.example.com:5349) relaying media for a participant behind a restrictive NAT or firewall, for whom a direct peer-to-peer connection fails. Public STUN servers are used for everyone else and need no configuration. Leave empty to serve calls with STUN only - most calls will still connect, but some participants may not."
131
+ "help": "A turn: or turns: URL (e.g. turns:turn.example.com:5349), or several separated by commas (turn:turn.example.com:3478,turns:turn.example.com:5349), relaying media for a participant behind a restrictive NAT or firewall, for whom a direct peer-to-peer connection fails. Public STUN servers are used for everyone else and need no configuration. Leave empty to serve calls with STUN only - most calls will still connect, but some participants may not."
132
132
  },
133
133
  {
134
134
  "key": "mail:videoconf:turn:username",