@streaming-cdn/rtc-web 1.3.13 → 1.3.15

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.
@@ -0,0 +1,1996 @@
1
+ const Ce = 0.02, Ie = 1.4;
2
+ function ie(r, e, t = 0.02, i = 1.4) {
3
+ const n = r.filter((o) => o.level >= t);
4
+ if (n.length === 0) return null;
5
+ const s = n.reduce((o, c) => c.level > o.level ? c : o), a = n.find((o) => o.participantId === e);
6
+ return a ? s.participantId === e ? e : s.level >= a.level * i ? s.participantId : e : s.participantId;
7
+ }
8
+ function ne(r) {
9
+ const e = [];
10
+ for (const t of r) {
11
+ if (String(t.type ?? "") !== "inbound-rtp" || String(t.kind ?? t.mediaType ?? "") !== "audio") continue;
12
+ const i = Number(t.audioLevel ?? NaN);
13
+ Number.isFinite(i) && e.push({ mid: typeof t.mid == "string" ? t.mid : null, level: i });
14
+ }
15
+ return e;
16
+ }
17
+ const M = {
18
+ audio: { active: !1, maxBitrate: 0, maxFramerate: 0, scaleResolutionDownBy: 4 },
19
+ low: { active: !0, maxBitrate: 35e4, maxFramerate: 15, scaleResolutionDownBy: 4 },
20
+ medium: { active: !0, maxBitrate: 11e5, maxFramerate: 24, scaleResolutionDownBy: 2 },
21
+ high: { active: !0, maxBitrate: 28e5, maxFramerate: 30, scaleResolutionDownBy: 1 }
22
+ }, ae = {
23
+ warmupSamples: 3,
24
+ poorSamplesToStepDown: 2,
25
+ goodSamplesToStepUp: 6,
26
+ criticalLossPct: 8,
27
+ criticalRttMs: 800,
28
+ poorLossPct: 3,
29
+ poorRttMs: 350,
30
+ goodLossPct: 1,
31
+ goodRttMs: 250,
32
+ sustainRatio: 0.8
33
+ };
34
+ function re(r) {
35
+ const e = String(r ?? "").trim().toLowerCase();
36
+ return !e || e === "none" ? "" : e === "cpu" || e === "bandwidth" ? e : "other";
37
+ }
38
+ function oe(r, e, t, i, n = {}) {
39
+ const s = { ...ae, ...n.thresholds }, a = ["low", "medium", "high"], o = n.ceiling && n.ceiling !== "audio" ? n.ceiling : "high", c = a.indexOf(o), d = a.indexOf(r), l = (n.samplesSeen ?? s.warmupSamples) + 1, h = l <= s.warmupSamples, v = re(e.qualityLimitationReason), g = e.packetLossPct ?? 0, k = e.rttMs ?? 0, p = e.availableOutgoingBitrateBps, C = M[r].maxBitrate, S = g >= s.criticalLossPct || k >= s.criticalRttMs || v === "cpu", y = S || g >= s.poorLossPct || k >= s.poorRttMs || v === "bandwidth", T = p == null || p >= C * s.sustainRatio, I = g < s.goodLossPct && k < s.goodRttMs && !v && T, R = y ? t + 1 : 0, E = I ? i + 1 : 0, f = { quality: r, poorSamples: R, goodSamples: E, samplesSeen: l, changed: !1 };
40
+ return d > c ? { quality: a[c], poorSamples: 0, goodSamples: 0, samplesSeen: l, changed: !0 } : h ? f : S && d > 0 ? { quality: a[d - 1], poorSamples: 0, goodSamples: 0, samplesSeen: l, changed: !0 } : R >= s.poorSamplesToStepDown && d > 0 ? { quality: a[d - 1], poorSamples: 0, goodSamples: 0, samplesSeen: l, changed: !0 } : E >= s.goodSamplesToStepUp && d < c ? { quality: a[d + 1], poorSamples: 0, goodSamples: 0, samplesSeen: l, changed: !0 } : f;
41
+ }
42
+ function ce(r, e) {
43
+ const t = r instanceof Map ? r : new Map(r);
44
+ let i = 0, n = 0, s = null, a = null, o = null;
45
+ for (const l of t.values()) {
46
+ const h = String(l.type ?? "");
47
+ if (h === "outbound-rtp" && String(l.kind ?? "") === "video" && (i += Number(l.packetsSent ?? 0), s = l.qualityLimitationReason ?? s), h === "remote-inbound-rtp" && String(l.kind ?? "") === "video") {
48
+ n += Number(l.packetsLost ?? 0);
49
+ const v = Number(l.roundTripTime ?? NaN);
50
+ Number.isFinite(v) && (a = v * 1e3);
51
+ }
52
+ if (h === "candidate-pair" && (l.nominated === !0 || String(l.state ?? "") === "succeeded")) {
53
+ const v = Number(l.currentRoundTripTime ?? NaN);
54
+ a == null && Number.isFinite(v) && (a = v * 1e3);
55
+ const g = Number(l.availableOutgoingBitrate ?? NaN);
56
+ Number.isFinite(g) && (o = g);
57
+ }
58
+ }
59
+ const c = { packetsSent: i, packetsLost: n, timestamp: Date.now() };
60
+ let d = null;
61
+ if (e) {
62
+ const l = i - e.packetsSent, h = n - e.packetsLost;
63
+ l > 0 && h >= 0 && (d = h / (l + h) * 100);
64
+ }
65
+ return {
66
+ sample: { packetLossPct: d, rttMs: a, availableOutgoingBitrateBps: o, qualityLimitationReason: s },
67
+ counters: c
68
+ };
69
+ }
70
+ const le = {
71
+ audio: { active: !1, maxBitrate: 0, maxFramerate: 0, scaleResolutionDownBy: 4 }
72
+ }, de = { active: !0, maxBitrate: 4e6, maxFramerate: 30, scaleResolutionDownBy: 1 };
73
+ class Q {
74
+ options;
75
+ socket = null;
76
+ localStream = null;
77
+ screenStream = null;
78
+ peers = /* @__PURE__ */ new Map();
79
+ joinPromise = null;
80
+ resolveJoin = null;
81
+ rejectJoin = null;
82
+ joined = !1;
83
+ stopped = !1;
84
+ reconnectAttempt = 0;
85
+ reconnectTimer = null;
86
+ heartbeatTimer = null;
87
+ qualityTimer = null;
88
+ speakerTimer = null;
89
+ activeSpeaker = null;
90
+ heartbeatDeadline = 0;
91
+ quality;
92
+ preferredVideoCodec;
93
+ signalQueue = Promise.resolve();
94
+ socketGeneration = 0;
95
+ constructor(e) {
96
+ this.options = e, this.localStream = e.localStream ?? null, this.quality = e.quality ?? "high", this.preferredVideoCodec = e.preferredVideoCodec ?? "auto";
97
+ }
98
+ async initialize() {
99
+ if (!globalThis.RTCPeerConnection || !globalThis.WebSocket)
100
+ throw new Error("This browser does not support WebRTC");
101
+ }
102
+ async join() {
103
+ if (!this.joined) {
104
+ if (this.joinPromise) return this.joinPromise;
105
+ this.joinPromise = this.connect();
106
+ try {
107
+ await this.joinPromise;
108
+ } finally {
109
+ this.joinPromise = null;
110
+ }
111
+ }
112
+ }
113
+ async leave() {
114
+ this.stopped = !0, this.clearConnectionTimers(), this.send({ type: "rtc.leave" }), this.joined = !1, this.socket?.close(1e3, "Participant left"), this.socket = null;
115
+ for (const e of this.peers.values()) e.connection.close();
116
+ this.peers.clear(), this.stopStream(this.localStream), this.stopStream(this.screenStream), this.localStream = null, this.screenStream = null;
117
+ }
118
+ setMuted(e) {
119
+ for (const t of this.localStream?.getAudioTracks() ?? []) t.enabled = !e;
120
+ }
121
+ async setCameraEnabled(e) {
122
+ if (e && !this.localStream?.getVideoTracks().length) {
123
+ const i = (await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 30, max: 30 } } })).getVideoTracks()[0];
124
+ if (!i) throw new Error("Camera is unavailable");
125
+ this.localStream || (this.localStream = new MediaStream()), this.localStream.addTrack(i), this.screenStream?.getVideoTracks().some((n) => n.readyState === "live") || await this.replaceVideoTrack(i);
126
+ return;
127
+ }
128
+ for (const t of this.localStream?.getVideoTracks() ?? []) t.enabled = e;
129
+ }
130
+ async setScreenShareEnabled(e) {
131
+ if (!e) {
132
+ const n = this.screenStream;
133
+ this.screenStream = null, this.stopStream(n);
134
+ const s = this.localStream?.getVideoTracks()[0] ?? null;
135
+ await this.replaceVideoTrack(s), this.options.onEvent("screensharechange", { enabled: !1, stream: null });
136
+ return;
137
+ }
138
+ if (this.screenStream?.getVideoTracks()[0]?.readyState === "live") return;
139
+ const t = await navigator.mediaDevices.getDisplayMedia({
140
+ video: { width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 30, max: 30 } },
141
+ audio: !1
142
+ }), i = t.getVideoTracks()[0];
143
+ if (!i) throw new Error("Screen sharing is unavailable");
144
+ i.contentHint = "detail", this.screenStream = t, i.addEventListener("ended", () => {
145
+ this.screenStream?.getVideoTracks()[0]?.id === i.id && this.setScreenShareEnabled(!1);
146
+ });
147
+ try {
148
+ await this.replaceVideoTrack(i), this.options.onEvent("screensharechange", { enabled: !0, stream: t });
149
+ } catch (n) {
150
+ throw this.screenStream = null, this.stopStream(t), n;
151
+ }
152
+ }
153
+ async setDevice(e) {
154
+ const t = { deviceId: { exact: e.deviceId } }, i = await navigator.mediaDevices.getUserMedia(e.kind === "audioinput" ? { audio: t } : { video: { ...t, width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 30, max: 30 } } }), n = e.kind === "audioinput" ? i.getAudioTracks()[0] : i.getVideoTracks()[0];
155
+ if (!n) throw new Error("Selected media device is unavailable");
156
+ const s = e.kind === "audioinput" ? "audio" : "video", a = this.localStream?.getTracks().find((o) => o.kind === s);
157
+ if (this.localStream || (this.localStream = new MediaStream()), a && (this.localStream.removeTrack(a), a.stop()), this.localStream.addTrack(n), s === "video") {
158
+ this.screenStream?.getVideoTracks().some((o) => o.readyState === "live") || await this.replaceVideoTrack(n);
159
+ return;
160
+ }
161
+ for (const o of this.peers.values()) {
162
+ const c = o.connection.getSenders().find((d) => d.track?.kind === s);
163
+ c ? await c.replaceTrack(n) : o.connection.addTrack(n, this.localStream);
164
+ }
165
+ await this.applyVideoQuality();
166
+ }
167
+ async setQuality(e) {
168
+ this.quality = e;
169
+ for (const t of this.localStream?.getVideoTracks() ?? []) t.enabled = e !== "audio";
170
+ for (const t of this.peers.values())
171
+ t.quality = Q.clampToCeiling(t.quality, e), t.poorSamples = 0, t.goodSamples = 0;
172
+ await this.applyVideoQuality();
173
+ }
174
+ static clampToCeiling(e, t) {
175
+ if (t === "audio") return e;
176
+ const i = ["low", "medium", "high"];
177
+ return i.indexOf(e) > i.indexOf(t) ? t : e;
178
+ }
179
+ async setPreferredVideoCodec(e) {
180
+ this.preferredVideoCodec = e;
181
+ for (const t of this.peers.values()) this.applyCodecPreference(t.connection);
182
+ await this.renegotiateAll();
183
+ }
184
+ getParticipants() {
185
+ return [...this.peers.values()].map((e) => e.identity);
186
+ }
187
+ getLocalStream() {
188
+ return this.localStream;
189
+ }
190
+ async setVideoTrack(e) {
191
+ if (e.kind !== "video") throw new Error("A video track is required");
192
+ this.localStream || (this.localStream = new MediaStream());
193
+ const t = this.localStream.getVideoTracks()[0];
194
+ t?.id !== e.id && (t && this.localStream.removeTrack(t), this.localStream.addTrack(e)), await this.replaceVideoTrack(e), this.options.onEvent("localstream", { stream: this.localStream, track: e });
195
+ }
196
+ getRemoteStreams() {
197
+ return [...this.peers.values()].map((e) => ({ participant: e.identity, stream: e.stream }));
198
+ }
199
+ getPrimaryPeerConnection() {
200
+ return this.peers.values().next().value?.connection ?? null;
201
+ }
202
+ sendChat(e, t) {
203
+ this.send({ type: "rtc.chat", message: e, participantIds: t ?? [] });
204
+ }
205
+ sendControl(e, t = {}) {
206
+ this.send({ type: "rtc.control", action: e, detail: t });
207
+ }
208
+ async connect() {
209
+ const { credential: e } = this.options;
210
+ if (!e.signalingToken || !e.signalingUrl || !e.roomId || !e.participantId)
211
+ throw new Error("A room signaling credential is required");
212
+ this.stopped = !1, !this.localStream && (this.options.audio || this.options.video) && (this.localStream = await navigator.mediaDevices.getUserMedia({ audio: this.options.audio, video: this.options.video })), this.localStream && this.options.onEvent("localstream", { stream: this.localStream }), await this.openSocket(!1);
213
+ }
214
+ async openSocket(e) {
215
+ const { credential: t } = this.options, i = new URL(t.signalingUrl, globalThis.location?.href);
216
+ i.protocol = i.protocol === "https:" ? "wss:" : "ws:";
217
+ const n = new Promise((c, d) => {
218
+ this.resolveJoin = c, this.rejectJoin = d;
219
+ }), s = new WebSocket(i, ["rtc-client", t.signalingToken]), a = ++this.socketGeneration;
220
+ this.socket = s;
221
+ const o = setTimeout(() => this.rejectJoin?.(new Error("RTC signaling timed out")), 15e3);
222
+ s.addEventListener("open", () => {
223
+ this.heartbeatDeadline = Date.now() + 45e3, this.startHeartbeat(s), this.startQualitySampling(), this.send({ type: "rtc.room.join" });
224
+ }), s.addEventListener("message", (c) => {
225
+ const d = String(c.data);
226
+ this.signalQueue = this.signalQueue.then(async () => {
227
+ this.socket !== s || this.socketGeneration !== a || await this.handleSignal(d);
228
+ }).catch((l) => {
229
+ const h = l instanceof Error ? l.message : "RTC signaling processing failed";
230
+ this.options.onEvent("signalingerror", { message: h }), this.joined || this.rejectJoin?.(new Error(h));
231
+ });
232
+ }), s.addEventListener("error", () => this.rejectJoin?.(new Error("RTC signaling connection failed"))), s.addEventListener("close", (c) => {
233
+ if (this.socket !== s) return;
234
+ this.socket = null, this.stopHeartbeat(), this.stopQualitySampling(), !this.joined && !e && this.rejectJoin?.(new Error(c.reason || "RTC signaling connection closed"));
235
+ const d = !this.stopped && (this.joined || e);
236
+ this.joined = !1, d ? (this.resetPeers(), this.options.onEvent("reconnecting", { reason: c.reason, code: c.code, attempt: this.reconnectAttempt + 1 }), this.scheduleReconnect()) : this.stopped || this.options.onEvent("disconnected", { reason: c.reason, code: c.code });
237
+ });
238
+ try {
239
+ await n, this.joined = !0, this.options.onEvent("connected", { participantId: t.participantId, resumed: e });
240
+ } finally {
241
+ clearTimeout(o), this.resolveJoin = null, this.rejectJoin = null;
242
+ }
243
+ }
244
+ async handleSignal(e) {
245
+ let t;
246
+ try {
247
+ t = JSON.parse(e);
248
+ } catch {
249
+ return;
250
+ }
251
+ const i = String(t.type ?? "");
252
+ if (i !== "ready") {
253
+ if (i === "pong") {
254
+ this.heartbeatDeadline = Date.now() + 45e3;
255
+ return;
256
+ }
257
+ if (i === "rtc.room.peers") {
258
+ this.reconnectAttempt = 0;
259
+ const n = Array.isArray(t.peers) ? t.peers : [];
260
+ for (const s of n) this.ensurePeer(s);
261
+ this.resolveJoin?.();
262
+ for (const s of n)
263
+ this.shouldInitiateOffer(s) && await this.createOffer(s);
264
+ return;
265
+ }
266
+ if (i === "rtc.room.join" && t.sender) {
267
+ this.options.onEvent("participantjoined", t.sender), this.ensurePeer(t.sender), this.shouldInitiateOffer(t.sender) && await this.createOffer(t.sender);
268
+ return;
269
+ }
270
+ if (i === "rtc.offer" && t.sender && t.description) {
271
+ const n = this.ensurePeer(t.sender);
272
+ n.offerTask && await n.offerTask;
273
+ const s = n.connection.signalingState !== "stable", a = !this.shouldInitiateOffer(t.sender);
274
+ if (s && !a) return;
275
+ s && await n.connection.setLocalDescription({ type: "rollback" }), await n.connection.setRemoteDescription(t.description);
276
+ const o = await n.connection.createAnswer();
277
+ await n.connection.setLocalDescription(o), this.send({
278
+ type: "rtc.answer",
279
+ targetParticipantId: t.sender.participantId,
280
+ negotiationId: t.negotiationId,
281
+ description: n.connection.localDescription
282
+ });
283
+ return;
284
+ }
285
+ if (i === "rtc.answer" && t.sender && t.description) {
286
+ const n = this.peers.get(t.sender.participantId);
287
+ if (!n || n.connection.signalingState !== "have-local-offer" || t.negotiationId && n.activeNegotiationId && t.negotiationId !== n.activeNegotiationId) return;
288
+ await n.connection.setRemoteDescription(t.description), n.activeNegotiationId = null;
289
+ return;
290
+ }
291
+ if (i === "rtc.ice" && t.sender && t.candidate) {
292
+ await this.ensurePeer(t.sender).connection.addIceCandidate(t.candidate).catch(() => {
293
+ });
294
+ return;
295
+ }
296
+ if ((i === "rtc.peer.left" || i === "rtc.leave") && t.sender) {
297
+ this.removePeer(t.sender.participantId);
298
+ return;
299
+ }
300
+ i === "rtc.chat" ? this.options.onEvent("chatupdate", t) : i === "rtc.control" && this.options.onEvent("control", t);
301
+ }
302
+ }
303
+ ensurePeer(e) {
304
+ const t = this.peers.get(e.participantId);
305
+ if (t) return t;
306
+ const i = new RTCPeerConnection({
307
+ iceServers: this.options.credential.iceServers ?? [],
308
+ iceTransportPolicy: this.options.iceTransportPolicy ?? "all"
309
+ }), n = new MediaStream(), s = {
310
+ identity: e,
311
+ connection: i,
312
+ stream: n,
313
+ makingOffer: !1,
314
+ offerTask: null,
315
+ activeNegotiationId: null,
316
+ quality: Q.startingQuality(this.quality),
317
+ poorSamples: 0,
318
+ goodSamples: 0,
319
+ samplesSeen: 0,
320
+ counters: null
321
+ };
322
+ this.peers.set(e.participantId, s);
323
+ for (const c of this.localStream?.getAudioTracks() ?? []) i.addTrack(c, this.localStream);
324
+ const a = this.screenStream?.getVideoTracks().some((c) => c.readyState === "live") ? this.screenStream : this.localStream, o = a?.getVideoTracks()[0];
325
+ return o && a && (s.videoSender = i.addTrack(o, a)), !this.localStream?.getAudioTracks().length && this.options.receiveAudio && i.addTransceiver("audio", { direction: "recvonly" }), !o && this.options.receiveVideo && (s.videoTransceiver = i.addTransceiver("video", { direction: "recvonly" }), s.videoSender = s.videoTransceiver.sender), this.applyCodecPreference(i), this.applyVideoQuality(i), i.addEventListener("icecandidate", (c) => {
326
+ c.candidate && this.send({ type: "rtc.ice", targetParticipantId: e.participantId, candidate: c.candidate.toJSON() });
327
+ }), i.addEventListener("track", (c) => {
328
+ n.getTracks().some((d) => d.id === c.track.id) || n.addTrack(c.track), this.options.onEvent("remotestream", { participant: e, stream: n, track: c.track });
329
+ }), i.addEventListener("connectionstatechange", () => {
330
+ ["failed", "closed"].includes(i.connectionState) && this.removePeer(e.participantId), this.options.onEvent("peerconnectionstate", { participant: e, state: i.connectionState });
331
+ }), s;
332
+ }
333
+ async createOffer(e) {
334
+ const t = this.ensurePeer(e);
335
+ if (t.offerTask) return t.offerTask;
336
+ if (t.connection.signalingState !== "stable") return;
337
+ const i = this.performOffer(t);
338
+ t.offerTask = i;
339
+ try {
340
+ await i;
341
+ } finally {
342
+ t.offerTask === i && (t.offerTask = null);
343
+ }
344
+ }
345
+ async performOffer(e) {
346
+ e.makingOffer = !0;
347
+ try {
348
+ const t = await e.connection.createOffer({
349
+ offerToReceiveAudio: this.options.receiveAudio,
350
+ offerToReceiveVideo: this.options.receiveVideo
351
+ });
352
+ if (e.connection.signalingState !== "stable") return;
353
+ await e.connection.setLocalDescription(t);
354
+ const i = H();
355
+ e.activeNegotiationId = i, this.send({
356
+ type: "rtc.offer",
357
+ targetParticipantId: e.identity.participantId,
358
+ negotiationId: i,
359
+ description: e.connection.localDescription
360
+ });
361
+ } finally {
362
+ e.makingOffer = !1;
363
+ }
364
+ }
365
+ shouldInitiateOffer(e) {
366
+ return this.options.credential.participantId.localeCompare(e.participantId) < 0;
367
+ }
368
+ async renegotiateAll() {
369
+ for (const e of this.peers.values()) await this.createOffer(e.identity);
370
+ }
371
+ async replaceVideoTrack(e) {
372
+ let t = !1;
373
+ for (const i of this.peers.values()) {
374
+ const n = i.videoSender ?? i.connection.getSenders().find((a) => a.track?.kind === "video") ?? i.connection.getTransceivers().find((a) => a.receiver.track.kind === "video")?.sender, s = i.videoTransceiver ?? i.connection.getTransceivers().find((a) => a.sender === n);
375
+ if (n) {
376
+ const a = e ? this.options.receiveVideo ? "sendrecv" : "sendonly" : this.options.receiveVideo ? "recvonly" : "inactive";
377
+ s && s.direction !== a && (s.direction = a, t = !0), await n.replaceTrack(e), i.videoSender = n, s && (i.videoTransceiver = s);
378
+ } else e && (i.videoSender = i.connection.addTrack(e, this.screenStream ?? this.localStream ?? new MediaStream([e])), t = !0);
379
+ }
380
+ t && await this.renegotiateAll(), await this.applyVideoQuality();
381
+ }
382
+ async applyVideoQuality(e) {
383
+ const t = e ? [...this.peers.values()].filter((i) => i.connection === e) : [...this.peers.values()];
384
+ for (const i of t) await this.applyPeerVideoQuality(i);
385
+ }
386
+ /**
387
+ * Applies this connection's own tier. The room-wide setting is a ceiling
388
+ * rather than an instruction: asking for audio keeps every link on audio, but
389
+ * asking for high lets each link settle wherever it can actually hold.
390
+ */
391
+ async applyPeerVideoQuality(e) {
392
+ const t = this.screenStream?.getVideoTracks()[0]?.id;
393
+ for (const i of e.connection.getSenders().filter((n) => n.track?.kind === "video")) {
394
+ const n = !!(t && i.track?.id === t), s = n ? de : this.quality === "audio" ? le.audio : M[e.quality], a = i.getParameters(), o = a.encodings?.length ? a.encodings : [{}];
395
+ a.encodings = o.map((c) => ({ ...c, ...s })), a.degradationPreference = n ? "maintain-resolution" : "maintain-framerate", await i.setParameters(a);
396
+ }
397
+ }
398
+ startQualitySampling() {
399
+ this.stopQualitySampling(), this.qualityTimer = setInterval(() => {
400
+ this.samplePeerQuality();
401
+ }, 5e3), this.speakerTimer = setInterval(() => {
402
+ this.sampleActiveSpeaker();
403
+ }, 1e3);
404
+ }
405
+ stopQualitySampling() {
406
+ this.qualityTimer && clearInterval(this.qualityTimer), this.qualityTimer = null, this.speakerTimer && clearInterval(this.speakerTimer), this.speakerTimer = null;
407
+ }
408
+ async sampleActiveSpeaker() {
409
+ const e = [];
410
+ for (const i of [...this.peers.values()])
411
+ if (i.connection.connectionState === "connected")
412
+ try {
413
+ const n = await i.connection.getStats();
414
+ for (const s of ne(n.values()))
415
+ e.push({ participantId: i.identity.participantId, level: s.level });
416
+ } catch {
417
+ }
418
+ const t = ie(e, this.activeSpeaker);
419
+ t !== this.activeSpeaker && (this.activeSpeaker = t, this.options.onEvent("activespeaker", { participantId: t }));
420
+ }
421
+ async samplePeerQuality() {
422
+ if (this.quality !== "audio")
423
+ for (const e of [...this.peers.values()]) {
424
+ if (e.connection.connectionState !== "connected") continue;
425
+ let t;
426
+ try {
427
+ t = await e.connection.getStats();
428
+ } catch {
429
+ continue;
430
+ }
431
+ const { sample: i, counters: n } = ce(t, e.counters);
432
+ e.counters = n;
433
+ const s = oe(e.quality, i, e.poorSamples, e.goodSamples, {
434
+ samplesSeen: e.samplesSeen,
435
+ ceiling: this.quality
436
+ });
437
+ e.poorSamples = s.poorSamples, e.goodSamples = s.goodSamples, e.samplesSeen = s.samplesSeen, s.changed && (e.quality = s.quality, await this.applyPeerVideoQuality(e).catch(() => {
438
+ }), this.options.onEvent("peerquality", { participantId: e.identity.participantId, quality: s.quality }));
439
+ }
440
+ }
441
+ /** A new link starts one step below the ceiling so it proves itself upward. */
442
+ static startingQuality(e) {
443
+ return e === "low" ? "low" : "medium";
444
+ }
445
+ applyCodecPreference(e) {
446
+ if (this.preferredVideoCodec === "auto" || !globalThis.RTCRtpReceiver?.getCapabilities) return;
447
+ const t = RTCRtpReceiver.getCapabilities("video")?.codecs ?? [], i = `video/${this.preferredVideoCodec}`, n = [
448
+ ...t.filter((s) => s.mimeType.toLowerCase() === i),
449
+ ...t.filter((s) => s.mimeType.toLowerCase() !== i)
450
+ ];
451
+ for (const s of e.getTransceivers())
452
+ s.receiver.track.kind === "video" && typeof s.setCodecPreferences == "function" && s.setCodecPreferences(n);
453
+ }
454
+ removePeer(e) {
455
+ const t = this.peers.get(e);
456
+ t && (t.connection.close(), this.peers.delete(e), this.options.onEvent("participantleft", t.identity));
457
+ }
458
+ resetPeers() {
459
+ for (const e of this.peers.values()) e.connection.close();
460
+ this.peers.clear();
461
+ }
462
+ startHeartbeat(e) {
463
+ this.stopHeartbeat(), this.stopQualitySampling(), this.heartbeatTimer = setInterval(() => {
464
+ if (!(this.socket !== e || e.readyState !== WebSocket.OPEN)) {
465
+ if (Date.now() > this.heartbeatDeadline) {
466
+ e.close(4e3, "Signaling heartbeat timed out");
467
+ return;
468
+ }
469
+ e.send("ping");
470
+ }
471
+ }, 2e4);
472
+ }
473
+ stopHeartbeat() {
474
+ this.heartbeatTimer && clearInterval(this.heartbeatTimer), this.heartbeatTimer = null;
475
+ }
476
+ clearConnectionTimers() {
477
+ this.stopHeartbeat(), this.stopQualitySampling(), this.reconnectTimer && clearTimeout(this.reconnectTimer), this.reconnectTimer = null;
478
+ }
479
+ scheduleReconnect() {
480
+ if (this.reconnectTimer || this.stopped) return;
481
+ const e = Math.min(3e4, 1e3 * 2 ** Math.min(this.reconnectAttempt, 5)) + Math.round(Math.random() * 300);
482
+ this.reconnectAttempt += 1, this.reconnectTimer = setTimeout(() => {
483
+ this.reconnectTimer = null, this.openSocket(!0).catch((t) => {
484
+ this.options.onEvent("signalingerror", { message: t instanceof Error ? t.message : "RTC signaling connection failed" }), this.scheduleReconnect();
485
+ });
486
+ }, e);
487
+ }
488
+ send(e) {
489
+ this.socket?.readyState === WebSocket.OPEN && this.socket.send(JSON.stringify({
490
+ clientMessageId: e.clientMessageId ?? H(),
491
+ ...e
492
+ }));
493
+ }
494
+ stopStream(e) {
495
+ for (const t of e?.getTracks() ?? []) t.stop();
496
+ }
497
+ }
498
+ function H() {
499
+ return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
500
+ }
501
+ const U = { active: !0, maxBitrate: 4e6, maxFramerate: 30, scaleResolutionDownBy: 1 }, _ = [
502
+ { rid: "a", maxBitrate: M.high.maxBitrate, maxFramerate: M.high.maxFramerate },
503
+ { rid: "b", maxBitrate: M.medium.maxBitrate, maxFramerate: M.medium.maxFramerate, scaleResolutionDownBy: 2 },
504
+ { rid: "c", maxBitrate: M.low.maxBitrate, maxFramerate: M.low.maxFramerate, scaleResolutionDownBy: 4 }
505
+ ], W = { high: "a", medium: "b", low: "c" };
506
+ class he {
507
+ options;
508
+ apiBase;
509
+ socket = null;
510
+ connection = null;
511
+ sessionId = null;
512
+ localStream = null;
513
+ screenStream = null;
514
+ remotes = /* @__PURE__ */ new Map();
515
+ midToParticipant = /* @__PURE__ */ new Map();
516
+ // Announcements can arrive while our own session is still being created —
517
+ // existing publishers re-announce the moment they see our join — and must
518
+ // not be dropped, because nobody will repeat them. Keyed by participant so
519
+ // a duplicate replaces rather than queues: processing the same announcement
520
+ // twice used to race the first subscribe and draw an SFU 406.
521
+ pendingAnnouncements = /* @__PURE__ */ new Map();
522
+ videoSender = null;
523
+ joined = !1;
524
+ stopped = !1;
525
+ reconnectAttempt = 0;
526
+ reconnectTimer = null;
527
+ heartbeatTimer = null;
528
+ heartbeatDeadline = 0;
529
+ speakerTimer = null;
530
+ activeSpeaker = null;
531
+ quality;
532
+ resolveJoin = null;
533
+ rejectJoin = null;
534
+ signalQueue = Promise.resolve();
535
+ negotiationQueue = Promise.resolve();
536
+ socketGeneration = 0;
537
+ constructor(e) {
538
+ this.options = e, this.localStream = e.localStream ?? null, this.quality = e.quality ?? "high", this.apiBase = new URL(e.credential.signalingUrl, globalThis.location?.href).origin;
539
+ }
540
+ async initialize() {
541
+ if (!globalThis.RTCPeerConnection || !globalThis.WebSocket)
542
+ throw new Error("This browser does not support WebRTC");
543
+ }
544
+ async join() {
545
+ if (this.joined) return;
546
+ const { credential: e } = this.options;
547
+ if (!e.signalingToken || !e.signalingUrl || !e.roomId || !e.participantId)
548
+ throw new Error("A room signaling credential is required");
549
+ this.stopped = !1, !this.localStream && (this.options.audio || this.options.video) && (this.localStream = await navigator.mediaDevices.getUserMedia({ audio: this.options.audio, video: this.options.video })), this.localStream && this.options.onEvent("localstream", { stream: this.localStream }), await this.openSocket(!1), await this.publish(), this.startSpeakerSampling(), this.announceTracks();
550
+ const t = [...this.pendingAnnouncements.values()];
551
+ this.pendingAnnouncements.clear();
552
+ for (const i of t)
553
+ await this.handleTrackAnnouncement(i.sender, i.detail);
554
+ }
555
+ async leave() {
556
+ this.stopped = !0, this.clearTimers(), this.send({ type: "rtc.leave" }), this.joined = !1, this.socket?.close(1e3, "Participant left"), this.socket = null, await this.closePublishedTracks().catch(() => {
557
+ }), this.connection?.close(), this.connection = null, this.sessionId = null, this.remotes.clear(), this.midToParticipant.clear(), this.stopStream(this.localStream), this.stopStream(this.screenStream), this.localStream = null, this.screenStream = null;
558
+ }
559
+ setMuted(e) {
560
+ for (const t of this.localStream?.getAudioTracks() ?? []) t.enabled = !e;
561
+ }
562
+ async setCameraEnabled(e) {
563
+ if (e && !this.localStream?.getVideoTracks().length) {
564
+ const i = (await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 30, max: 30 } } })).getVideoTracks()[0];
565
+ if (!i) throw new Error("Camera is unavailable");
566
+ this.localStream || (this.localStream = new MediaStream()), this.localStream.addTrack(i), this.screenStream?.getVideoTracks().some((n) => n.readyState === "live") || await this.replaceVideoTrack(i);
567
+ return;
568
+ }
569
+ for (const t of this.localStream?.getVideoTracks() ?? []) t.enabled = e;
570
+ }
571
+ async setScreenShareEnabled(e) {
572
+ if (!e) {
573
+ const n = this.screenStream;
574
+ this.screenStream = null, this.stopStream(n);
575
+ const s = this.localStream?.getVideoTracks()[0] ?? null;
576
+ await this.replaceVideoTrack(s), this.options.onEvent("screensharechange", { enabled: !1, stream: null });
577
+ return;
578
+ }
579
+ if (this.screenStream?.getVideoTracks()[0]?.readyState === "live") return;
580
+ const t = await navigator.mediaDevices.getDisplayMedia({
581
+ video: { width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 30, max: 30 } },
582
+ audio: !1
583
+ }), i = t.getVideoTracks()[0];
584
+ if (!i) throw new Error("Screen sharing is unavailable");
585
+ i.contentHint = "detail", this.screenStream = t, i.addEventListener("ended", () => {
586
+ this.screenStream?.getVideoTracks()[0]?.id === i.id && this.setScreenShareEnabled(!1);
587
+ });
588
+ try {
589
+ await this.replaceVideoTrack(i), this.options.onEvent("screensharechange", { enabled: !0, stream: t });
590
+ } catch (n) {
591
+ throw this.screenStream = null, this.stopStream(t), n;
592
+ }
593
+ }
594
+ async setDevice(e) {
595
+ const t = { deviceId: { exact: e.deviceId } }, i = await navigator.mediaDevices.getUserMedia(e.kind === "audioinput" ? { audio: t } : { video: { ...t, width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 30, max: 30 } } }), n = e.kind === "audioinput" ? i.getAudioTracks()[0] : i.getVideoTracks()[0];
596
+ if (!n) throw new Error("Selected media device is unavailable");
597
+ const s = e.kind === "audioinput" ? "audio" : "video", a = this.localStream?.getTracks().find((c) => c.kind === s);
598
+ if (this.localStream || (this.localStream = new MediaStream()), a && (this.localStream.removeTrack(a), a.stop()), this.localStream.addTrack(n), s === "video") {
599
+ this.screenStream?.getVideoTracks().some((c) => c.readyState === "live") || await this.replaceVideoTrack(n);
600
+ return;
601
+ }
602
+ const o = this.connection?.getSenders().find((c) => c.track?.kind === "audio");
603
+ o && await o.replaceTrack(n);
604
+ }
605
+ /** One uplink serves everyone, so the requested quality applies directly. */
606
+ async setQuality(e) {
607
+ this.quality = e;
608
+ for (const t of this.localStream?.getVideoTracks() ?? []) t.enabled = e !== "audio";
609
+ await this.applyVideoQuality();
610
+ }
611
+ async setPreferredVideoCodec(e) {
612
+ }
613
+ async setVideoTrack(e) {
614
+ if (e.kind !== "video") throw new Error("A video track is required");
615
+ this.localStream || (this.localStream = new MediaStream());
616
+ const t = this.localStream.getVideoTracks()[0];
617
+ t?.id !== e.id && (t && this.localStream.removeTrack(t), this.localStream.addTrack(e)), await this.replaceVideoTrack(e), this.options.onEvent("localstream", { stream: this.localStream, track: e });
618
+ }
619
+ getParticipants() {
620
+ return [...this.remotes.values()].map((e) => e.identity);
621
+ }
622
+ getLocalStream() {
623
+ return this.localStream;
624
+ }
625
+ getRemoteStreams() {
626
+ return [...this.remotes.values()].map((e) => ({ participant: e.identity, stream: e.stream }));
627
+ }
628
+ getPrimaryPeerConnection() {
629
+ return this.connection;
630
+ }
631
+ sendChat(e, t) {
632
+ this.send({ type: "rtc.chat", message: e, participantIds: t ?? [] });
633
+ }
634
+ sendControl(e, t = {}) {
635
+ this.send({ type: "rtc.control", action: e, detail: t });
636
+ }
637
+ // --- signaling -----------------------------------------------------------
638
+ async openSocket(e) {
639
+ const { credential: t } = this.options, i = new URL(t.signalingUrl, globalThis.location?.href);
640
+ i.protocol = i.protocol === "https:" ? "wss:" : "ws:";
641
+ const n = new Promise((c, d) => {
642
+ this.resolveJoin = c, this.rejectJoin = d;
643
+ }), s = new WebSocket(i, ["rtc-client", t.signalingToken]), a = ++this.socketGeneration;
644
+ this.socket = s;
645
+ const o = setTimeout(() => this.rejectJoin?.(new Error("RTC signaling timed out")), 15e3);
646
+ s.addEventListener("open", () => {
647
+ this.heartbeatDeadline = Date.now() + 45e3, this.startHeartbeat(s), this.send({ type: "rtc.room.join" });
648
+ }), s.addEventListener("message", (c) => {
649
+ const d = String(c.data);
650
+ this.signalQueue = this.signalQueue.then(async () => {
651
+ this.socket !== s || this.socketGeneration !== a || await this.handleSignal(d);
652
+ }).catch((l) => {
653
+ const h = l instanceof Error ? l.message : "RTC signaling processing failed";
654
+ this.options.onEvent("signalingerror", { message: h }), this.joined || this.rejectJoin?.(new Error(h));
655
+ });
656
+ }), s.addEventListener("error", () => this.rejectJoin?.(new Error("RTC signaling connection failed"))), s.addEventListener("close", (c) => {
657
+ if (this.socket !== s) return;
658
+ this.socket = null, this.stopHeartbeat(), !this.joined && !e && this.rejectJoin?.(new Error(c.reason || "RTC signaling connection closed"));
659
+ const d = !this.stopped && (this.joined || e);
660
+ this.joined = !1, d ? (this.options.onEvent("reconnecting", { reason: c.reason, code: c.code, attempt: this.reconnectAttempt + 1 }), this.scheduleReconnect()) : this.stopped || this.options.onEvent("disconnected", { reason: c.reason, code: c.code });
661
+ });
662
+ try {
663
+ await n, this.joined = !0, this.options.onEvent("connected", { participantId: t.participantId, resumed: e });
664
+ } finally {
665
+ clearTimeout(o), this.resolveJoin = null, this.rejectJoin = null;
666
+ }
667
+ }
668
+ async handleSignal(e) {
669
+ let t;
670
+ try {
671
+ t = JSON.parse(e);
672
+ } catch {
673
+ return;
674
+ }
675
+ const i = String(t.type ?? "");
676
+ if (i !== "ready") {
677
+ if (i === "pong") {
678
+ this.heartbeatDeadline = Date.now() + 45e3;
679
+ return;
680
+ }
681
+ if (i === "rtc.room.peers") {
682
+ this.reconnectAttempt = 0, this.resolveJoin?.();
683
+ return;
684
+ }
685
+ if (i === "rtc.room.join" && t.sender) {
686
+ this.options.onEvent("participantjoined", t.sender), this.sessionId && this.announceTracks();
687
+ return;
688
+ }
689
+ if ((i === "rtc.peer.left" || i === "rtc.leave") && t.sender) {
690
+ this.removeRemote(t.sender.participantId);
691
+ return;
692
+ }
693
+ if (i === "rtc.control") {
694
+ if (String(t.action ?? "") === "sfu.tracks" && t.sender) {
695
+ await this.handleTrackAnnouncement(t.sender, t.detail);
696
+ return;
697
+ }
698
+ this.options.onEvent("control", t);
699
+ return;
700
+ }
701
+ i === "rtc.chat" && this.options.onEvent("chatupdate", t);
702
+ }
703
+ }
704
+ // --- SFU media -----------------------------------------------------------
705
+ async publish() {
706
+ const e = [
707
+ ...this.localStream?.getAudioTracks() ?? [],
708
+ ...this.localStream?.getVideoTracks() ?? []
709
+ ], t = new RTCPeerConnection({
710
+ iceServers: this.options.credential.iceServers ?? [],
711
+ iceTransportPolicy: this.options.iceTransportPolicy ?? "all"
712
+ });
713
+ this.connection = t, t.addEventListener("track", (o) => this.handleRemoteTrack(o)), t.addEventListener("connectionstatechange", () => {
714
+ this.options.onEvent("peerconnectionstate", { participant: this.selfIdentity(), state: t.connectionState });
715
+ });
716
+ const i = await this.sfuFetch("POST", "/v1/rtc/sfu/sessions");
717
+ if (this.sessionId = i.sessionId, e.length === 0) return;
718
+ const n = e.map((o) => t.addTransceiver(o, {
719
+ direction: "sendonly",
720
+ streams: this.localStream ? [this.localStream] : [],
721
+ ...o.kind === "video" ? { sendEncodings: _.map((c) => ({ ...c })) } : {}
722
+ })), s = await t.createOffer();
723
+ await t.setLocalDescription(s);
724
+ const a = await this.sfuFetch(
725
+ "POST",
726
+ `/v1/rtc/sfu/sessions/${encodeURIComponent(i.sessionId)}/tracks`,
727
+ {
728
+ sessionDescription: t.localDescription,
729
+ tracks: n.map((o) => ({
730
+ location: "local",
731
+ mid: o.mid,
732
+ trackName: this.trackName(o.sender.track)
733
+ }))
734
+ }
735
+ );
736
+ a.sessionDescription && await t.setRemoteDescription(a.sessionDescription), this.videoSender = t.getSenders().find((o) => o.track?.kind === "video") ?? null, await this.applyVideoQuality();
737
+ }
738
+ trackName(e) {
739
+ return `${this.options.credential.participantId}/${e.kind}`;
740
+ }
741
+ announceTracks() {
742
+ if (!this.sessionId) return;
743
+ const e = [
744
+ ...(this.localStream?.getAudioTracks() ?? []).map((t) => ({ trackName: this.trackName(t), kind: t.kind, simulcast: !1 })),
745
+ ...(this.localStream?.getVideoTracks() ?? []).map((t) => ({ trackName: this.trackName(t), kind: t.kind, simulcast: !0 }))
746
+ ];
747
+ e.length !== 0 && this.sendControl("sfu.tracks", { sessionId: this.sessionId, tracks: e });
748
+ }
749
+ async handleTrackAnnouncement(e, t) {
750
+ const i = String(t?.sessionId ?? ""), n = (t?.tracks ?? []).map((o) => ({ trackName: String(o.trackName ?? ""), simulcast: o.simulcast === !0 })).filter((o) => !!o.trackName), s = n.map((o) => o.trackName);
751
+ if (!i || s.length === 0 || e.participantId === this.options.credential.participantId) return;
752
+ if (!this.connection || !this.sessionId) {
753
+ this.pendingAnnouncements.set(e.participantId, { sender: e, detail: t ?? {} });
754
+ return;
755
+ }
756
+ const a = this.remotes.get(e.participantId) ?? {
757
+ identity: e,
758
+ stream: new MediaStream(),
759
+ sessionId: i,
760
+ trackNames: [],
761
+ simulcastNames: /* @__PURE__ */ new Set(),
762
+ mids: /* @__PURE__ */ new Map()
763
+ };
764
+ a.sessionId = i;
765
+ for (const o of n)
766
+ o.simulcast && a.simulcastNames.add(o.trackName);
767
+ this.remotes.set(e.participantId, a), this.negotiationQueue = this.negotiationQueue.then(async () => {
768
+ const o = s.filter((c) => !a.trackNames.includes(c));
769
+ o.length !== 0 && await this.subscribe(a, o);
770
+ }).catch((o) => {
771
+ this.options.onEvent("signalingerror", { message: o instanceof Error ? o.message : "SFU subscribe failed" });
772
+ }), await this.negotiationQueue;
773
+ }
774
+ async subscribe(e, t) {
775
+ const i = this.connection;
776
+ if (!i || !this.sessionId || t.length === 0) return;
777
+ const n = await this.sfuFetch("POST", `/v1/rtc/sfu/sessions/${encodeURIComponent(this.sessionId)}/tracks`, {
778
+ tracks: t.map((s) => ({
779
+ location: "remote",
780
+ sessionId: e.sessionId,
781
+ trackName: s,
782
+ // Start on the full layer and let the SFU fall down the alphabet when
783
+ // it is not available; a deliberate switch goes through
784
+ // setRemoteQuality instead.
785
+ ...e.simulcastNames.has(s) ? { simulcast: { preferredRid: "a", priorityOrdering: "asciibetical", ridNotAvailable: "asciibetical" } } : {}
786
+ }))
787
+ });
788
+ for (const s of n.tracks ?? [])
789
+ s.mid && (e.mids.set(s.mid, String(s.trackName ?? "")), this.midToParticipant.set(s.mid, e.identity.participantId));
790
+ if (e.trackNames = [.../* @__PURE__ */ new Set([...e.trackNames, ...t])], n.requiresImmediateRenegotiation && n.sessionDescription) {
791
+ await i.setRemoteDescription(n.sessionDescription);
792
+ const s = await i.createAnswer();
793
+ await i.setLocalDescription(s), await this.sfuFetch("PUT", `/v1/rtc/sfu/sessions/${encodeURIComponent(this.sessionId)}/renegotiate`, {
794
+ sessionDescription: i.localDescription
795
+ });
796
+ }
797
+ }
798
+ handleRemoteTrack(e) {
799
+ const t = e.transceiver?.mid ?? null, i = t ? this.midToParticipant.get(t) : void 0, n = i ? this.remotes.get(i) : void 0;
800
+ n && (n.stream.getTracks().some((s) => s.id === e.track.id) || n.stream.addTrack(e.track), this.options.onEvent("remotestream", { participant: n.identity, stream: n.stream, track: e.track }));
801
+ }
802
+ removeRemote(e) {
803
+ const t = this.remotes.get(e);
804
+ if (t) {
805
+ for (const i of t.mids.keys()) this.midToParticipant.delete(i);
806
+ this.remotes.delete(e), this.options.onEvent("participantleft", t.identity);
807
+ }
808
+ }
809
+ /**
810
+ * Switches which simulcast layer the SFU forwards for one participant's
811
+ * video — for example a thumbnail tile has no use for 720p. This changes
812
+ * only what this client receives; the publisher keeps sending every layer.
813
+ */
814
+ async setRemoteQuality(e, t) {
815
+ const i = this.remotes.get(e);
816
+ if (!i || !this.sessionId) return;
817
+ const n = [...i.mids.entries()].find(([, s]) => i.simulcastNames.has(s))?.[0];
818
+ n && await this.sfuFetch("PUT", `/v1/rtc/sfu/sessions/${encodeURIComponent(this.sessionId)}/tracks/update`, {
819
+ tracks: [{
820
+ mid: n,
821
+ simulcast: { preferredRid: W[t], priorityOrdering: "asciibetical", ridNotAvailable: "asciibetical" }
822
+ }]
823
+ });
824
+ }
825
+ async replaceVideoTrack(e) {
826
+ this.videoSender && (await this.videoSender.replaceTrack(e), await this.applyVideoQuality());
827
+ }
828
+ async applyVideoQuality() {
829
+ const e = this.videoSender;
830
+ if (!e) return;
831
+ const t = this.screenStream?.getVideoTracks()[0]?.id, i = !!(t && e.track?.id === t), n = e.getParameters(), s = n.encodings?.length ? n.encodings : [{}];
832
+ if (s.length > 1) {
833
+ const a = this.quality === "audio" ? null : W[this.quality];
834
+ n.encodings = s.map((o) => {
835
+ if (i)
836
+ return { ...o, active: o.rid === "a", ...o.rid === "a" ? { maxBitrate: U.maxBitrate, maxFramerate: U.maxFramerate } : {} };
837
+ const c = _.find((l) => l.rid === o.rid), d = a != null && String(o.rid ?? "") >= a;
838
+ return { ...o, ...c ?? {}, active: d };
839
+ });
840
+ } else {
841
+ const a = this.quality === "audio" ? M.audio : M[this.quality], o = i ? U : a;
842
+ n.encodings = s.map((c) => ({ ...c, ...o }));
843
+ }
844
+ n.degradationPreference = i ? "maintain-resolution" : "maintain-framerate", await e.setParameters(n);
845
+ }
846
+ async closePublishedTracks() {
847
+ const e = this.connection;
848
+ if (!e || !this.sessionId) return;
849
+ const t = e.getTransceivers().filter((i) => i.sender.track && i.mid).map((i) => ({ mid: i.mid }));
850
+ t.length !== 0 && await this.sfuFetch("PUT", `/v1/rtc/sfu/sessions/${encodeURIComponent(this.sessionId)}/tracks/close`, {
851
+ tracks: t,
852
+ force: !0
853
+ });
854
+ }
855
+ selfIdentity() {
856
+ return {
857
+ participantId: this.options.credential.participantId,
858
+ externalUserId: this.options.credential.externalUserId ?? this.options.credential.participantId,
859
+ displayName: this.options.credential.externalUserId ?? this.options.credential.participantId,
860
+ platform: "web"
861
+ };
862
+ }
863
+ async sfuFetch(e, t, i) {
864
+ const n = await fetch(`${this.apiBase}${t}`, {
865
+ method: e,
866
+ headers: {
867
+ authorization: `Bearer ${this.options.credential.signalingToken}`,
868
+ "content-type": "application/json"
869
+ },
870
+ body: i === void 0 ? void 0 : JSON.stringify(i)
871
+ });
872
+ if (!n.ok) throw new Error(`SFU request failed with HTTP ${n.status}`);
873
+ return n.json();
874
+ }
875
+ // --- plumbing shared with the mesh ---------------------------------------
876
+ startHeartbeat(e) {
877
+ this.stopHeartbeat(), this.heartbeatTimer = setInterval(() => {
878
+ if (!(this.socket !== e || e.readyState !== WebSocket.OPEN)) {
879
+ if (Date.now() > this.heartbeatDeadline) {
880
+ e.close(4e3, "Signaling heartbeat timed out");
881
+ return;
882
+ }
883
+ e.send("ping");
884
+ }
885
+ }, 2e4);
886
+ }
887
+ stopHeartbeat() {
888
+ this.heartbeatTimer && clearInterval(this.heartbeatTimer), this.heartbeatTimer = null;
889
+ }
890
+ startSpeakerSampling() {
891
+ this.stopSpeakerSampling(), this.speakerTimer = setInterval(() => {
892
+ this.sampleActiveSpeaker();
893
+ }, 1e3);
894
+ }
895
+ stopSpeakerSampling() {
896
+ this.speakerTimer && clearInterval(this.speakerTimer), this.speakerTimer = null;
897
+ }
898
+ async sampleActiveSpeaker() {
899
+ const e = this.connection;
900
+ if (!e || e.connectionState !== "connected") return;
901
+ let t;
902
+ try {
903
+ t = await e.getStats();
904
+ } catch {
905
+ return;
906
+ }
907
+ const i = [];
908
+ for (const s of ne(t.values())) {
909
+ const a = s.mid ? this.midToParticipant.get(s.mid) : void 0;
910
+ a && i.push({ participantId: a, level: s.level });
911
+ }
912
+ const n = ie(i, this.activeSpeaker);
913
+ n !== this.activeSpeaker && (this.activeSpeaker = n, this.options.onEvent("activespeaker", { participantId: n }));
914
+ }
915
+ clearTimers() {
916
+ this.stopHeartbeat(), this.stopSpeakerSampling(), this.reconnectTimer && clearTimeout(this.reconnectTimer), this.reconnectTimer = null;
917
+ }
918
+ scheduleReconnect() {
919
+ if (this.reconnectTimer || this.stopped) return;
920
+ const e = Math.min(3e4, 1e3 * 2 ** Math.min(this.reconnectAttempt, 5)) + Math.round(Math.random() * 300);
921
+ this.reconnectAttempt += 1, this.reconnectTimer = setTimeout(() => {
922
+ this.reconnectTimer = null, this.openSocket(!0).then(() => this.announceTracks()).catch((t) => {
923
+ this.options.onEvent("signalingerror", { message: t instanceof Error ? t.message : "RTC signaling connection failed" }), this.scheduleReconnect();
924
+ });
925
+ }, e);
926
+ }
927
+ send(e) {
928
+ this.socket?.readyState === WebSocket.OPEN && this.socket.send(JSON.stringify({
929
+ clientMessageId: e.clientMessageId ?? ue(),
930
+ ...e
931
+ }));
932
+ }
933
+ stopStream(e) {
934
+ for (const t of e?.getTracks() ?? []) t.stop();
935
+ }
936
+ }
937
+ function ue() {
938
+ return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
939
+ }
940
+ function Ee(r, e = {}) {
941
+ const t = e.document ?? globalThis.document, i = !!(t && t.pictureInPictureEnabled);
942
+ let n = null;
943
+ function s() {
944
+ return n || (n = t.createElement("video"), n.muted = !0, n.playsInline = !0, n.style.position = "fixed", n.style.width = "1px", n.style.height = "1px", n.style.opacity = "0", n.style.pointerEvents = "none", n.addEventListener("leavepictureinpicture", () => e.onLeave?.()), t.body.appendChild(n), n);
945
+ }
946
+ function a(o) {
947
+ const c = r.getRemoteStreams();
948
+ return o ? c.find((d) => d.participant.participantId === o)?.stream ?? null : c[0]?.stream ?? r.getLocalStream();
949
+ }
950
+ return {
951
+ supported: i,
952
+ async enter(o) {
953
+ if (!i) throw new Error("This browser does not support picture-in-picture");
954
+ const c = a(o);
955
+ if (!c) throw new Error("No video stream is available for picture-in-picture");
956
+ const d = s();
957
+ d.srcObject !== c && (d.srcObject = c), await d.play().catch(() => {
958
+ }), await d.requestPictureInPicture();
959
+ },
960
+ async exit() {
961
+ t.pictureInPictureElement && typeof t.exitPictureInPicture == "function" && await t.exitPictureInPicture();
962
+ },
963
+ dispose() {
964
+ n?.remove(), n = null;
965
+ }
966
+ };
967
+ }
968
+ function Re(r, e) {
969
+ const t = e.document ?? globalThis.document, i = e.fetchImplementation ?? globalThis.fetch.bind(globalThis), n = new URL(e.credential.signalingUrl, globalThis.location?.href).origin, s = e.mimeType ?? "video/webm", a = e.height ?? 720, o = Math.round(a * 16 / 9);
970
+ let c = null, d = null, l = null, h = null, v = 0, g = [], k = !1;
971
+ async function p(C, S, y, T = "application/json") {
972
+ const I = await i(`${n}${S}`, {
973
+ method: C,
974
+ headers: {
975
+ authorization: `Bearer ${e.credential.signalingToken}`,
976
+ "content-type": T
977
+ },
978
+ body: y === void 0 ? void 0 : T === "application/json" ? JSON.stringify(y) : y
979
+ });
980
+ if (!I.ok) throw new Error(`Recording request failed with HTTP ${I.status}`);
981
+ return I.json();
982
+ }
983
+ return {
984
+ get recording() {
985
+ return k;
986
+ },
987
+ async start() {
988
+ if (k) return;
989
+ h = (await p("POST", "/v1/rtc/recordings")).recordingId;
990
+ const S = t.createElement("canvas");
991
+ S.width = o, S.height = a;
992
+ const y = S.getContext("2d"), T = /* @__PURE__ */ new Map(), I = (b, P) => {
993
+ let w = T.get(b);
994
+ return w || (w = t.createElement("video"), w.muted = !0, w.playsInline = !0, w.srcObject = P, w.play().catch(() => {
995
+ }), T.set(b, w)), w;
996
+ };
997
+ d = setInterval(() => {
998
+ const b = r.getRemoteStreams(), P = r.getLocalStream(), w = [
999
+ ...b.map((D) => ({ key: D.participant.participantId, stream: D.stream, label: D.participant.displayName })),
1000
+ ...P ? [{ key: "local", stream: P, label: "me" }] : []
1001
+ ];
1002
+ y.fillStyle = "#101418", y.fillRect(0, 0, o, a);
1003
+ const N = Math.max(1, Math.ceil(Math.sqrt(w.length))), m = Math.max(1, Math.ceil(w.length / N)), A = o / N, V = a / m;
1004
+ w.forEach((D, z) => {
1005
+ const J = I(D.key, D.stream), j = z % N * A, B = Math.floor(z / N) * V;
1006
+ J.videoWidth > 0 && y.drawImage(J, j + 2, B + 2, A - 4, V - 4), y.fillStyle = "rgba(0,0,0,.6)", y.fillRect(j + 8, B + V - 30, Math.min(A - 16, 12 + D.label.length * 9), 22), y.fillStyle = "#fff", y.font = "14px system-ui", y.fillText(D.label, j + 14, B + V - 14);
1007
+ });
1008
+ }, 66);
1009
+ const R = S.captureStream(15);
1010
+ l = new AudioContext();
1011
+ const E = l.createMediaStreamDestination(), f = /* @__PURE__ */ new Set(), u = () => {
1012
+ const b = [
1013
+ ...r.getRemoteStreams().map((P) => P.stream),
1014
+ ...r.getLocalStream() ? [r.getLocalStream()] : []
1015
+ ];
1016
+ for (const P of b)
1017
+ for (const w of P.getAudioTracks())
1018
+ f.has(w.id) || (f.add(w.id), l.createMediaStreamSource(new MediaStream([w])).connect(E));
1019
+ };
1020
+ u();
1021
+ const q = setInterval(u, 1e3), L = () => clearInterval(q);
1022
+ for (const b of E.stream.getAudioTracks()) R.addTrack(b);
1023
+ c = (e.mediaRecorderFactory ?? ((b, P) => new MediaRecorder(b, P)))(R, { mimeType: s }), g = [], c.addEventListener("dataavailable", (b) => {
1024
+ b.data && b.data.size > 0 && g.push(b.data);
1025
+ }), c.addEventListener("stop", L), c.start(1e3), v = Date.now(), k = !0;
1026
+ },
1027
+ async stop() {
1028
+ if (!k || !c || !h) throw new Error("The recorder is not running");
1029
+ k = !1;
1030
+ const C = c, S = new Promise((E) => C.addEventListener("stop", () => E(), { once: !0 }));
1031
+ C.stop(), await S, d && clearInterval(d), d = null, await l?.close().catch(() => {
1032
+ }), l = null;
1033
+ const y = new Blob(g, { type: s });
1034
+ g = [];
1035
+ const T = Math.max(1, Math.round((Date.now() - v) / 1e3)), I = await p(
1036
+ "PUT",
1037
+ `/v1/rtc/recordings/${encodeURIComponent(h)}/media`,
1038
+ y,
1039
+ s
1040
+ );
1041
+ await p("POST", `/v1/rtc/recordings/${encodeURIComponent(h)}/complete`, { durationSeconds: T });
1042
+ const R = { recordingId: h, sizeBytes: I.sizeBytes, durationSeconds: T };
1043
+ return h = null, c = null, R;
1044
+ },
1045
+ dispose() {
1046
+ d && clearInterval(d), d = null;
1047
+ try {
1048
+ c?.stop();
1049
+ } catch {
1050
+ }
1051
+ c = null, l?.close().catch(() => {
1052
+ }), l = null, k = !1;
1053
+ }
1054
+ };
1055
+ }
1056
+ const me = ["none", "blur", "aurora", "cybergrid", "bokeh", "sunset", "image"], pe = ["none", "snow", "confetti", "vignette"], G = {
1057
+ aurora(r, e, t, i) {
1058
+ const n = r.createLinearGradient(0, 0, 0, t);
1059
+ n.addColorStop(0, "#050b1e"), n.addColorStop(1, "#0b2540"), r.fillStyle = n, r.fillRect(0, 0, e, t);
1060
+ for (let s = 0; s < 3; s += 1) {
1061
+ r.beginPath();
1062
+ for (let a = 0; a <= e; a += 16) {
1063
+ const o = t * 0.35 + s * 40 + Math.sin(a / 140 + i / 900 + s) * 60;
1064
+ a === 0 ? r.moveTo(a, o) : r.lineTo(a, o);
1065
+ }
1066
+ r.lineTo(e, 0), r.lineTo(0, 0), r.closePath(), r.fillStyle = `hsla(${140 + s * 40 + Math.sin(i / 1300) * 20}, 80%, 55%, 0.14)`, r.fill();
1067
+ }
1068
+ },
1069
+ cybergrid(r, e, t, i) {
1070
+ r.fillStyle = "#0a0118", r.fillRect(0, 0, e, t), r.strokeStyle = "rgba(255,0,180,0.5)", r.lineWidth = 1.5;
1071
+ const n = t * 0.45;
1072
+ for (let a = 0; a < 12; a += 1) {
1073
+ const o = n + (a * 34 + i / 18) % (t - n);
1074
+ r.beginPath(), r.moveTo(0, o), r.lineTo(e, o), r.stroke();
1075
+ }
1076
+ for (let a = -8; a <= 8; a += 1)
1077
+ r.beginPath(), r.moveTo(e / 2 + a * 24, n), r.lineTo(e / 2 + a * e * 0.12, t), r.stroke();
1078
+ const s = r.createLinearGradient(0, t * 0.1, 0, n);
1079
+ s.addColorStop(0, "#ffd34d"), s.addColorStop(1, "#ff2d78"), r.fillStyle = s, r.beginPath(), r.arc(e / 2, n, t * 0.22, Math.PI, 0), r.fill();
1080
+ },
1081
+ bokeh(r, e, t, i) {
1082
+ r.fillStyle = "#14100c", r.fillRect(0, 0, e, t);
1083
+ for (let n = 0; n < 26; n += 1) {
1084
+ const s = (Math.sin(n * 12.9898) * 0.5 + 0.5) * e, a = (Math.sin(n * 78.233) * 0.5 + 0.5) * t + Math.sin(i / 1500 + n) * 12, o = 14 + n % 5 * 12;
1085
+ r.fillStyle = `hsla(${35 + n % 4 * 12}, 90%, 60%, ${0.1 + n % 3 * 0.05})`, r.beginPath(), r.arc(s, a, o, 0, Math.PI * 2), r.fill();
1086
+ }
1087
+ },
1088
+ sunset(r, e, t, i) {
1089
+ const n = r.createLinearGradient(0, 0, 0, t);
1090
+ n.addColorStop(0, "#2b1055"), n.addColorStop(0.55, "#d1548a"), n.addColorStop(1, "#ffb36b"), r.fillStyle = n, r.fillRect(0, 0, e, t), r.fillStyle = "#ffdf8e", r.beginPath(), r.arc(e * 0.5, t * 0.62 + Math.sin(i / 4e3) * 4, t * 0.13, 0, Math.PI * 2), r.fill(), r.fillStyle = "rgba(20,8,40,0.85)", r.fillRect(0, t * 0.78, e, t * 0.22);
1091
+ }
1092
+ };
1093
+ function Pe(r, e = {}) {
1094
+ const t = e.document ?? globalThis.document, i = e.width ?? 1280, n = e.height ?? 720, s = e.frameRate ?? 24, a = r instanceof MediaStreamTrack ? r : r.getVideoTracks()[0];
1095
+ if (!a) throw new Error("A video track is required");
1096
+ let o = e.background ?? "none", c = e.overlay ?? "none", d = e.backgroundImage ?? null, l = e.segmenter ?? null, h = null, v = !1;
1097
+ const g = t.createElement("video");
1098
+ g.muted = !0, g.playsInline = !0, g.srcObject = new MediaStream([a]), g.play().catch(() => {
1099
+ });
1100
+ const k = t.createElement("canvas");
1101
+ k.width = i, k.height = n;
1102
+ const p = k.getContext("2d"), C = t.createElement("canvas");
1103
+ C.width = i, C.height = n;
1104
+ const S = C.getContext("2d"), y = Array.from({ length: 80 }, (f, u) => ({
1105
+ x: (Math.sin(u * 12.9898) * 0.5 + 0.5) * i,
1106
+ y: (Math.sin(u * 78.233) * 0.5 + 0.5) * n,
1107
+ speed: 0.6 + u % 5 * 0.5,
1108
+ drift: Math.sin(u) * 0.6,
1109
+ size: 2 + u % 4,
1110
+ hue: u * 47 % 360
1111
+ }));
1112
+ function T() {
1113
+ if (h) {
1114
+ S.clearRect(0, 0, i, n), S.drawImage(h, 0, 0, i, n), S.globalCompositeOperation = "source-in", S.drawImage(g, 0, 0, i, n), S.globalCompositeOperation = "source-over", p.drawImage(C, 0, 0);
1115
+ return;
1116
+ }
1117
+ const f = i * 0.42, u = f * 9 / 16, q = i - f - 24, L = n - u - 24;
1118
+ p.save(), p.shadowColor = "rgba(0,0,0,.5)", p.shadowBlur = 24, p.beginPath(), p.roundRect(q, L, f, u, 14), p.clip(), p.drawImage(g, q, L, f, u), p.restore();
1119
+ }
1120
+ function I(f) {
1121
+ if (o === "none" ? p.drawImage(g, 0, 0, i, n) : o === "blur" ? (p.filter = "blur(14px)", p.drawImage(g, -16, -16, i + 32, n + 32), p.filter = "none", h && T()) : o === "image" && d ? (p.drawImage(d, 0, 0, i, n), T()) : ((G[o] ?? G.aurora)(p, i, n, f), T()), c === "snow" || c === "confetti")
1122
+ for (const u of y)
1123
+ u.y += u.speed * (c === "confetti" ? 1.6 : 1), u.x += u.drift, u.y > n && (u.y = -6, u.x = Math.random() * i), p.fillStyle = c === "snow" ? "rgba(255,255,255,.85)" : `hsl(${u.hue}, 90%, 60%)`, c === "confetti" ? p.fillRect(u.x, u.y, u.size + 2, u.size) : (p.beginPath(), p.arc(u.x, u.y, u.size / 2, 0, Math.PI * 2), p.fill());
1124
+ else if (c === "vignette") {
1125
+ const u = p.createRadialGradient(i / 2, n / 2, n * 0.4, i / 2, n / 2, n * 0.85);
1126
+ u.addColorStop(0, "rgba(0,0,0,0)"), u.addColorStop(1, "rgba(0,0,0,0.55)"), p.fillStyle = u, p.fillRect(0, 0, i, n);
1127
+ }
1128
+ }
1129
+ const R = setInterval(() => {
1130
+ I(Date.now()), l && !v && o !== "none" && (v = !0, l.segment(g).then((f) => {
1131
+ h = f;
1132
+ }).catch(() => {
1133
+ h = null;
1134
+ }).finally(() => {
1135
+ v = !1;
1136
+ }));
1137
+ }, Math.round(1e3 / s)), E = k.captureStream(s);
1138
+ return {
1139
+ track: E.getVideoTracks()[0],
1140
+ stream: E,
1141
+ setBackground(f, u) {
1142
+ o = me.includes(f) ? f : "none", u !== void 0 && (d = u), o === "none" && (h = null);
1143
+ },
1144
+ setOverlay(f) {
1145
+ c = pe.includes(f) ? f : "none";
1146
+ },
1147
+ setSegmenter(f) {
1148
+ l = f, f || (h = null);
1149
+ },
1150
+ dispose() {
1151
+ clearInterval(R), l?.close?.();
1152
+ for (const f of E.getTracks()) f.stop();
1153
+ g.srcObject = null;
1154
+ }
1155
+ };
1156
+ }
1157
+ class fe extends EventTarget {
1158
+ mode;
1159
+ options;
1160
+ client = null;
1161
+ peerConnection;
1162
+ state = "initialized";
1163
+ latestMediaScore = null;
1164
+ latestQuality = null;
1165
+ statsTimer = null;
1166
+ statsRunning = !1;
1167
+ previousCounters = null;
1168
+ lastTelemetryAt = 0;
1169
+ joinPromise = null;
1170
+ requestedQuality;
1171
+ effectiveQuality;
1172
+ poorQualitySamples = 0;
1173
+ goodQualitySamples = 0;
1174
+ constructor(e) {
1175
+ if (super(), !e.credential?.signalingToken || !e.credential.signalingUrl || !e.credential.roomId || !e.credential.participantId)
1176
+ throw new Error("A valid room signaling credential is required");
1177
+ this.mode = e.mode, this.requestedQuality = e.mode === "voice" ? "audio" : e.quality ?? "auto", this.effectiveQuality = this.requestedQuality === "auto" ? "medium" : this.requestedQuality, this.peerConnection = e.peerConnection ?? null, this.options = {
1178
+ ...e,
1179
+ audio: e.audio ?? !0,
1180
+ video: e.mode === "voice" ? !1 : e.video ?? !0,
1181
+ receiveAudio: e.receiveAudio ?? !0,
1182
+ receiveVideo: e.mode === "voice" ? !1 : e.receiveVideo ?? !0,
1183
+ statsInterval: Math.max(1e3, e.statsInterval ?? 5e3),
1184
+ telemetryInterval: Math.max(1e4, e.telemetryInterval ?? 3e4),
1185
+ autoReportStats: e.autoReportStats ?? !0
1186
+ };
1187
+ }
1188
+ async initialize() {
1189
+ if (this.client) return this;
1190
+ try {
1191
+ const t = (this.options.transport === "auto" ? this.mode === "meeting" ? "sfu" : "mesh" : this.options.transport ?? "mesh") === "sfu" ? he : Q;
1192
+ return this.client = new t({
1193
+ credential: {
1194
+ signalingToken: this.options.credential.signalingToken,
1195
+ signalingUrl: this.options.credential.signalingUrl,
1196
+ roomId: this.options.credential.roomId,
1197
+ participantId: this.options.credential.participantId,
1198
+ externalUserId: this.options.credential.externalUserId,
1199
+ iceServers: this.options.credential.iceServers
1200
+ },
1201
+ audio: this.options.audio,
1202
+ video: this.options.video,
1203
+ receiveAudio: this.options.receiveAudio,
1204
+ receiveVideo: this.options.receiveVideo,
1205
+ quality: this.effectiveQuality,
1206
+ preferredVideoCodec: this.options.preferredVideoCodec ?? "auto",
1207
+ iceTransportPolicy: this.options.iceTransportPolicy ?? "all",
1208
+ localStream: this.options.localStream,
1209
+ onEvent: (i, n) => this.handleNativeEvent(i, n)
1210
+ }), await this.client.initialize(), this.startStats(), this.emit("initialized", { mode: this.mode, sdkVersion: be }), this.options.autoJoin && await this.join(), this;
1211
+ } catch (e) {
1212
+ this.state = "failed";
1213
+ const t = te(e, "RTC initialization failed");
1214
+ throw this.emit("error", x(t)), t;
1215
+ }
1216
+ }
1217
+ async join() {
1218
+ const e = this.requireClient();
1219
+ return this.joinPromise ? this.joinPromise : (this.state = "joining", this.emit("joining", {}), this.joinPromise = e.join().catch((t) => {
1220
+ this.state = "failed";
1221
+ const i = te(t, "RTC connection failed");
1222
+ throw this.emit("error", x(i)), i;
1223
+ }).finally(() => {
1224
+ this.joinPromise = null;
1225
+ }), this.joinPromise);
1226
+ }
1227
+ async leave() {
1228
+ this.client && (await this.joinPromise?.catch(() => {
1229
+ }), await this.sampleAndReport("final").catch(() => {
1230
+ }), await this.client.leave(), this.state = "disconnected", this.stopStats());
1231
+ }
1232
+ async setMuted(e) {
1233
+ this.requireClient().setMuted(e), this.emit("mediachange", { kind: "audio", enabled: !e });
1234
+ }
1235
+ async setCameraEnabled(e) {
1236
+ if (this.mode === "voice" && e) throw new Error("Camera is unavailable in voice mode");
1237
+ await this.requireClient().setCameraEnabled(e), this.emit("mediachange", { kind: "video", enabled: e });
1238
+ }
1239
+ async setVideoTrack(e) {
1240
+ if (this.mode === "voice") throw new Error("Camera is unavailable in voice mode");
1241
+ await this.requireClient().setVideoTrack(e), this.emit("mediachange", { kind: "video", enabled: e.enabled, trackId: e.id });
1242
+ }
1243
+ async setScreenShareEnabled(e) {
1244
+ await this.requireClient().setScreenShareEnabled(e), this.emit("mediachange", { kind: "screenshare", enabled: e });
1245
+ }
1246
+ async setDevice(e) {
1247
+ await this.requireClient().setDevice(e), this.emit("devicechange", { deviceId: e.deviceId, kind: e.kind });
1248
+ }
1249
+ async setQuality(e) {
1250
+ if (this.mode === "voice" && e !== "audio") throw new Error("Voice calls only support audio quality");
1251
+ this.requestedQuality = e, this.poorQualitySamples = 0, this.goodQualitySamples = 0, await this.applyEffectiveQuality(e === "auto" ? "medium" : e, "manual");
1252
+ }
1253
+ async setPreferredVideoCodec(e) {
1254
+ await this.requireClient().setPreferredVideoCodec(e), this.emit("codecchange", { preferredVideoCodec: e });
1255
+ }
1256
+ async getDevices() {
1257
+ return navigator.mediaDevices?.enumerateDevices ? navigator.mediaDevices.enumerateDevices() : [];
1258
+ }
1259
+ getParticipants() {
1260
+ return this.requireClient().getParticipants().map(X);
1261
+ }
1262
+ getLocalStream() {
1263
+ return this.client?.getLocalStream() ?? null;
1264
+ }
1265
+ getRemoteStreams() {
1266
+ return (this.client?.getRemoteStreams() ?? []).map((e) => ({
1267
+ participant: X(e.participant),
1268
+ stream: e.stream
1269
+ }));
1270
+ }
1271
+ setPeerConnection(e) {
1272
+ this.peerConnection = e, this.previousCounters = null;
1273
+ }
1274
+ async sendChatMessage(e, t) {
1275
+ const i = e.trim();
1276
+ if (!i) throw new Error("message is required");
1277
+ this.requireClient().sendChat(i, t);
1278
+ }
1279
+ async sendControlMessage(e, t = {}) {
1280
+ const i = e.trim();
1281
+ if (!i) throw new Error("action is required");
1282
+ this.requireClient().sendControl(i, t);
1283
+ }
1284
+ async createPoll(e, t, i = {}) {
1285
+ const n = t.map((s) => s.trim()).filter(Boolean);
1286
+ if (!e.trim() || n.length < 2) throw new Error("A poll requires a question and at least two options");
1287
+ this.requireClient().sendControl("poll.create", {
1288
+ poll: {
1289
+ id: `poll-${crypto.randomUUID()}`,
1290
+ question: e.trim(),
1291
+ options: n,
1292
+ votes: {},
1293
+ ...i
1294
+ }
1295
+ });
1296
+ }
1297
+ async votePoll(e, t) {
1298
+ this.requireClient().sendControl("poll.vote", { pollId: e, optionIndex: t });
1299
+ }
1300
+ async requestStageAccess() {
1301
+ this.requireClient().sendControl("stage.request");
1302
+ }
1303
+ async cancelStageAccess() {
1304
+ this.requireClient().sendControl("stage.cancel");
1305
+ }
1306
+ async joinStage() {
1307
+ this.requireClient().sendControl("stage.join");
1308
+ }
1309
+ async leaveStage() {
1310
+ this.requireClient().sendControl("stage.leave");
1311
+ }
1312
+ async grantStageAccess(e) {
1313
+ this.requireClient().sendControl("stage.grant", { participantIds: e });
1314
+ }
1315
+ async denyStageAccess(e) {
1316
+ this.requireClient().sendControl("stage.deny", { participantIds: e });
1317
+ }
1318
+ async startRecording(e = !1) {
1319
+ this.requireClient().sendControl("recording.start", { allowMultiple: e });
1320
+ }
1321
+ async stopRecording(e) {
1322
+ this.requireClient().sendControl("recording.stop", { recordingId: e });
1323
+ }
1324
+ async pauseRecording(e) {
1325
+ this.requireClient().sendControl("recording.pause", { recordingId: e });
1326
+ }
1327
+ async resumeRecording(e) {
1328
+ this.requireClient().sendControl("recording.resume", { recordingId: e });
1329
+ }
1330
+ async runPreflight(e = {}) {
1331
+ const t = [], i = globalThis.isSecureContext === !0;
1332
+ i || t.push("secure_context_required");
1333
+ let n = "unavailable";
1334
+ if (e.requestMedia !== !1 && navigator.mediaDevices?.getUserMedia)
1335
+ try {
1336
+ (await navigator.mediaDevices.getUserMedia({ audio: this.options.audio, video: this.options.video })).getTracks().forEach((v) => v.stop()), n = "granted";
1337
+ } catch {
1338
+ n = "denied", t.push("media_permission_denied");
1339
+ }
1340
+ const s = navigator.mediaDevices?.enumerateDevices ? await navigator.mediaDevices.enumerateDevices().catch(() => []) : [], a = s.filter((h) => h.kind === "audioinput").length, o = s.filter((h) => h.kind === "videoinput").length;
1341
+ this.options.audio && a === 0 && t.push("microphone_unavailable"), this.options.video && o === 0 && t.push("camera_unavailable");
1342
+ const c = e.probeUrl ?? this.options.preflightProbeUrl;
1343
+ let d = null;
1344
+ if (c) {
1345
+ const h = performance.now();
1346
+ try {
1347
+ const v = await fetch(c, { method: "HEAD", cache: "no-store", credentials: "omit" });
1348
+ d = Math.round(performance.now() - h), v.ok || t.push("network_probe_failed");
1349
+ } catch {
1350
+ t.push("network_probe_failed");
1351
+ }
1352
+ }
1353
+ const l = {
1354
+ passed: t.length === 0,
1355
+ sampledAt: (/* @__PURE__ */ new Date()).toISOString(),
1356
+ secureContext: i,
1357
+ mediaPermission: n,
1358
+ microphoneCount: a,
1359
+ cameraCount: o,
1360
+ probeLatencyMs: d,
1361
+ effectiveConnectionType: Z()?.effectiveType ?? null,
1362
+ issues: t
1363
+ };
1364
+ return this.emit("preflight", l), this.options.autoReportStats && await this.reportQuality({
1365
+ sampleType: "preflight",
1366
+ sampledAt: l.sampledAt,
1367
+ connectionState: l.passed ? "ready" : "failed",
1368
+ rttMs: l.probeLatencyMs,
1369
+ effectiveConnectionType: l.effectiveConnectionType
1370
+ }).catch(() => {
1371
+ }), l;
1372
+ }
1373
+ async collectQualitySample(e = "runtime") {
1374
+ const t = this.options.statsProvider ? await this.options.statsProvider() : {}, i = this.peerConnection ? await ke(this.peerConnection, this.previousCounters) : null;
1375
+ i?.counters && (this.previousCounters = i.counters);
1376
+ const s = { ...{
1377
+ sampleType: e,
1378
+ sampledAt: (/* @__PURE__ */ new Date()).toISOString(),
1379
+ connectionState: this.peerConnection?.connectionState ?? this.state,
1380
+ mediaScore: this.latestMediaScore,
1381
+ networkScore: this.latestMediaScore == null ? 5 : Math.max(0, Math.min(5, Math.round(this.latestMediaScore))),
1382
+ estimatedMos: 4.5,
1383
+ rttMs: null,
1384
+ jitterMs: null,
1385
+ packetLossPct: null,
1386
+ packetLossCumulativePct: null,
1387
+ packetsLost: null,
1388
+ packetsReceived: null,
1389
+ inboundBitrateBps: null,
1390
+ outboundBitrateBps: null,
1391
+ availableOutgoingBitrateBps: null,
1392
+ framesPerSecond: null,
1393
+ frameWidth: null,
1394
+ frameHeight: null,
1395
+ framesDropped: null,
1396
+ freezeCount: null,
1397
+ freezeDurationMs: null,
1398
+ concealedSamples: null,
1399
+ codec: null,
1400
+ transportProtocol: null,
1401
+ candidateType: null,
1402
+ qualityLimitationReason: null,
1403
+ deviceMemoryGb: we(),
1404
+ effectiveConnectionType: Z()?.effectiveType ?? null
1405
+ }, ...i?.sample ?? {}, ...t, sampleType: e, sampledAt: (/* @__PURE__ */ new Date()).toISOString() }, a = Te(s);
1406
+ return s.networkScore = a.networkScore, s.estimatedMos = a.estimatedMos, this.latestQuality = s, s;
1407
+ }
1408
+ async reportQuality(e) {
1409
+ const t = this.options.credential.telemetryToken, i = this.options.telemetryEndpoint ?? this.options.credential.telemetryEndpoint;
1410
+ if (!t || !i) return;
1411
+ const n = await fetch(i, {
1412
+ method: "POST",
1413
+ headers: { authorization: `Bearer ${t}`, "content-type": "application/json" },
1414
+ body: JSON.stringify(e),
1415
+ keepalive: e.sampleType === "final"
1416
+ });
1417
+ if (!n.ok) throw new Error(`RTC telemetry failed with HTTP ${n.status}`);
1418
+ }
1419
+ getStats() {
1420
+ const e = this.client, t = e?.getLocalStream();
1421
+ return {
1422
+ state: this.state,
1423
+ participantCount: e ? e.getParticipants().length + 1 : 0,
1424
+ audioEnabled: !!t?.getAudioTracks().some((i) => i.enabled),
1425
+ videoEnabled: !!t?.getVideoTracks().some((i) => i.enabled),
1426
+ screenShareEnabled: !1,
1427
+ latestMediaScore: this.latestMediaScore,
1428
+ latestQuality: this.latestQuality,
1429
+ sampledAt: (/* @__PURE__ */ new Date()).toISOString()
1430
+ };
1431
+ }
1432
+ /** Escape hatch for advanced integrations. Prefer the stable SDK methods when possible. */
1433
+ getNativeClient() {
1434
+ return this.client;
1435
+ }
1436
+ destroy() {
1437
+ this.stopStats(), this.client && this.client.leave(), this.client = null, this.joinPromise = null, this.peerConnection = null, this.state = "disconnected";
1438
+ }
1439
+ handleNativeEvent(e, t) {
1440
+ e === "connected" ? (this.state = "connected", this.peerConnection = this.client?.getPrimaryPeerConnection() ?? null) : e === "disconnected" ? this.state = "disconnected" : e === "peerconnectionstate" && (this.peerConnection = this.client?.getPrimaryPeerConnection() ?? null), this.emit(e, t);
1441
+ }
1442
+ startStats() {
1443
+ this.stopStats(), this.statsTimer = setInterval(() => {
1444
+ this.sampleAndReport("runtime");
1445
+ }, this.options.statsInterval);
1446
+ }
1447
+ async sampleAndReport(e) {
1448
+ if (!this.statsRunning) {
1449
+ this.statsRunning = !0;
1450
+ try {
1451
+ const t = await this.collectQualitySample(e);
1452
+ await this.adaptQuality(t), this.emit("stats", { ...this.getStats(), latestQuality: t });
1453
+ const i = Date.now(), n = e !== "runtime" || i - this.lastTelemetryAt >= this.options.telemetryInterval;
1454
+ this.options.autoReportStats && n && (await this.reportQuality(t), this.lastTelemetryAt = i);
1455
+ } catch (t) {
1456
+ this.emit("telemetryerror", x(t));
1457
+ } finally {
1458
+ this.statsRunning = !1;
1459
+ }
1460
+ }
1461
+ }
1462
+ stopStats() {
1463
+ this.statsTimer !== null && clearInterval(this.statsTimer), this.statsTimer = null;
1464
+ }
1465
+ async adaptQuality(e) {
1466
+ if (this.requestedQuality !== "auto" || this.options.adaptiveQuality === !1 || this.mode === "voice") return;
1467
+ if ((this.client?.getParticipants().length ?? 0) > 1) {
1468
+ this.poorQualitySamples = 0, this.goodQualitySamples = 0, this.effectiveQuality !== "high" && await this.applyEffectiveQuality("high", "network");
1469
+ return;
1470
+ }
1471
+ const t = String(e.qualityLimitationReason ?? "").toLowerCase(), i = (e.packetLossPct ?? 0) >= 8 || (e.rttMs ?? 0) >= 800 || t === "cpu" || t === "bandwidth", n = i || (e.packetLossPct ?? 0) >= 3 || (e.rttMs ?? 0) >= 350 || e.availableOutgoingBitrateBps != null && e.availableOutgoingBitrateBps < 35e4, s = (e.packetLossPct ?? 100) < 1 && (e.rttMs ?? 1e3) < 180 && !t && (e.availableOutgoingBitrateBps == null || e.availableOutgoingBitrateBps > 1e6);
1472
+ this.poorQualitySamples = n ? this.poorQualitySamples + 1 : 0, this.goodQualitySamples = s ? this.goodQualitySamples + 1 : 0;
1473
+ const a = ["low", "medium", "high"], o = a.indexOf(this.effectiveQuality);
1474
+ i && o > 0 ? (this.poorQualitySamples = 0, await this.applyEffectiveQuality("low", "network")) : this.poorQualitySamples >= 2 && o > 0 ? (this.poorQualitySamples = 0, await this.applyEffectiveQuality(a[o - 1], "network")) : this.goodQualitySamples >= 6 && o >= 0 && o < a.length - 1 && (this.goodQualitySamples = 0, await this.applyEffectiveQuality(a[o + 1], "network"));
1475
+ }
1476
+ async applyEffectiveQuality(e, t) {
1477
+ if (this.effectiveQuality === e && this.client) return;
1478
+ const i = this.effectiveQuality;
1479
+ this.effectiveQuality = e, this.client && await this.client.setQuality(e), this.emit("qualitychange", { requestedQuality: this.requestedQuality, quality: e, previous: i, reason: t });
1480
+ }
1481
+ requireClient() {
1482
+ if (!this.client) throw new Error("Call initialize() before using the RTC client");
1483
+ return this.client;
1484
+ }
1485
+ emit(e, t) {
1486
+ const i = { type: e, detail: t, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
1487
+ this.dispatchEvent(new CustomEvent(e, { detail: i })), this.options.onEvent?.(i);
1488
+ }
1489
+ }
1490
+ async function Me(r) {
1491
+ return new fe(r).initialize();
1492
+ }
1493
+ let O = null;
1494
+ function se() {
1495
+ if (O) return O;
1496
+ const r = globalThis, e = r.AudioContext ?? r.webkitAudioContext;
1497
+ return e ? (O = new e(), O) : null;
1498
+ }
1499
+ async function ge() {
1500
+ const r = se();
1501
+ return r ? (r.state === "suspended" && await r.resume().catch(() => {
1502
+ }), r.state === "running") : !1;
1503
+ }
1504
+ class ve {
1505
+ options;
1506
+ media = null;
1507
+ cadenceTimer = null;
1508
+ oscillators = /* @__PURE__ */ new Set();
1509
+ constructor(e) {
1510
+ this.options = e;
1511
+ }
1512
+ unlock() {
1513
+ return ge();
1514
+ }
1515
+ startIncoming() {
1516
+ this.start("incoming", this.options.ringtoneUrl);
1517
+ }
1518
+ startRingback() {
1519
+ this.start("ringback", this.options.ringbackUrl);
1520
+ }
1521
+ stop() {
1522
+ this.cadenceTimer && clearInterval(this.cadenceTimer), this.cadenceTimer = null, this.media?.pause(), this.media && (this.media.currentTime = 0), this.media = null;
1523
+ for (const e of this.oscillators)
1524
+ try {
1525
+ e.stop();
1526
+ } catch {
1527
+ }
1528
+ this.oscillators.clear();
1529
+ }
1530
+ start(e, t) {
1531
+ if (this.stop(), this.options.sounds === !1) return;
1532
+ if (t && typeof Audio < "u") {
1533
+ this.media = new Audio(t), this.media.loop = !0, this.media.play().catch(() => {
1534
+ });
1535
+ return;
1536
+ }
1537
+ const i = () => this.playBuiltInTone(e);
1538
+ i(), this.cadenceTimer = setInterval(i, e === "incoming" ? 3e3 : 3500);
1539
+ }
1540
+ playBuiltInTone(e) {
1541
+ const t = se();
1542
+ if (!t || t.state !== "running") return;
1543
+ const i = e === "incoming" ? 1.25 : 1, n = e === "incoming" ? [440, 480] : [425], s = t.createGain(), a = t.currentTime;
1544
+ s.gain.setValueAtTime(1e-4, a), s.gain.exponentialRampToValueAtTime(0.045, a + 0.025), s.gain.setValueAtTime(0.045, a + i - 0.06), s.gain.exponentialRampToValueAtTime(1e-4, a + i), s.connect(t.destination);
1545
+ for (const o of n) {
1546
+ const c = t.createOscillator();
1547
+ c.type = "sine", c.frequency.value = o, c.connect(s), this.oscillators.add(c), c.onended = () => this.oscillators.delete(c), c.start(a), c.stop(a + i + 0.02);
1548
+ }
1549
+ }
1550
+ }
1551
+ class ye extends EventTarget {
1552
+ options;
1553
+ socket = null;
1554
+ stopped = !1;
1555
+ reconnectAttempt = 0;
1556
+ reconnectTimer = null;
1557
+ heartbeatTimer = null;
1558
+ heartbeatDeadline = 0;
1559
+ currentCall = null;
1560
+ outgoingCallId = null;
1561
+ mount = null;
1562
+ terminalTimer = null;
1563
+ tokenRefreshTimer = null;
1564
+ tokenRequest = null;
1565
+ currentToken;
1566
+ currentTokenExpiresAt;
1567
+ callAudio;
1568
+ mediaClients = /* @__PURE__ */ new Map();
1569
+ activeCallIds = /* @__PURE__ */ new Set();
1570
+ seenInvitationIds = /* @__PURE__ */ new Set();
1571
+ startCallRequest = null;
1572
+ lastDisconnect = null;
1573
+ constructor(e) {
1574
+ if (super(), !e.token && !e.tokenProvider) throw new Error("A client token or tokenProvider is required");
1575
+ if (e.token && !e.token.startsWith("rtcc_")) throw new Error("A valid RTC client token is required");
1576
+ if (!e.signalingUrl && !e.apiBaseUrl && !globalThis.location?.origin)
1577
+ throw new Error("signalingUrl or apiBaseUrl is required outside a browser");
1578
+ this.options = e, this.currentToken = e.token ?? null, this.currentTokenExpiresAt = e.tokenExpiresAt, this.callAudio = new ve(e), this.mount = K(e.mount), this.scheduleTokenRefresh();
1579
+ }
1580
+ connect() {
1581
+ if (!this.options.signalingUrl) throw new Error("signalingUrl is required to connect signaling");
1582
+ return this.stopped = !1, this.scheduleTokenRefresh(), this.openSocket("initial"), this;
1583
+ }
1584
+ setMount(e) {
1585
+ this.clearUi(), this.mount = K(e), this.currentCall && this.renderIncoming(this.currentCall);
1586
+ }
1587
+ /** Binds media lifetime to a call. Terminal call states automatically leave and destroy it. */
1588
+ attachMediaClient(e, t) {
1589
+ if (!e.trim()) throw new Error("callId is required");
1590
+ return this.mediaClients.set(e, t), () => {
1591
+ this.mediaClients.get(e) === t && this.mediaClients.delete(e);
1592
+ };
1593
+ }
1594
+ /**
1595
+ * Rings everyone in `targets` as a single call and connects whoever answers
1596
+ * first; the rest stop ringing on their own. `target` still works and means a
1597
+ * call with one recipient.
1598
+ */
1599
+ async startCall(e) {
1600
+ if (this.startCallRequest) return this.startCallRequest;
1601
+ if (this.activeCallIds.size > 0) throw new Error("Another call is already active");
1602
+ const t = e.targets?.length ? e.targets : e.target ? [e.target] : [];
1603
+ if (t.length === 0) throw new Error("startCall requires at least one target");
1604
+ const i = { mode: e.mode, targets: t, metadata: e.metadata, idempotencyKey: e.idempotencyKey }, n = (async () => {
1605
+ await this.callAudio.unlock(), this.callAudio.startRingback();
1606
+ let s;
1607
+ try {
1608
+ s = await this.request("/v1/rtc/client/calls", {
1609
+ method: "POST",
1610
+ headers: e.idempotencyKey ? { "idempotency-key": e.idempotencyKey } : void 0,
1611
+ body: JSON.stringify(i)
1612
+ });
1613
+ } catch (a) {
1614
+ throw this.callAudio.stop(), a;
1615
+ }
1616
+ return !s.credential && s.callerCredential && (s.credential = s.callerCredential), this.outgoingCallId = String(s.call?.id ?? "") || null, this.outgoingCallId && this.activeCallIds.add(this.outgoingCallId), this.emit("outgoingcall", s), s;
1617
+ })();
1618
+ this.startCallRequest = n;
1619
+ try {
1620
+ return await n;
1621
+ } finally {
1622
+ this.startCallRequest === n && (this.startCallRequest = null);
1623
+ }
1624
+ }
1625
+ async accept(e = this.currentCall?.id) {
1626
+ if (!e) throw new Error("No incoming call is available");
1627
+ const t = await this.callAction(e, "accept");
1628
+ return this.activeCallIds.add(e), this.callAudio.stop(), this.dismiss(e), this.emit("callaccepted", t), t;
1629
+ }
1630
+ async reject(e = this.currentCall?.id) {
1631
+ if (!e) throw new Error("No incoming call is available");
1632
+ const t = await this.callAction(e, "reject");
1633
+ return this.releaseMedia(e), this.showTerminal(e, "Call declined"), this.emit("callrejected", t), t;
1634
+ }
1635
+ async busy(e = this.currentCall?.id) {
1636
+ if (!e) throw new Error("No incoming call is available");
1637
+ const t = await this.callAction(e, "busy");
1638
+ return this.releaseMedia(e), this.showTerminal(e, "Line busy"), t;
1639
+ }
1640
+ async end(e) {
1641
+ this.callAudio.stop();
1642
+ const t = await this.callAction(e, "end");
1643
+ return this.releaseMedia(e), this.showTerminal(e, "Call ended"), t;
1644
+ }
1645
+ async cancel(e) {
1646
+ this.callAudio.stop();
1647
+ const t = await this.callAction(e, "cancel");
1648
+ return this.releaseMedia(e), this.showTerminal(e, "Call cancelled"), t;
1649
+ }
1650
+ /** Injects an incoming call received through the application's own push or signaling channel. */
1651
+ handleIncomingCall(e) {
1652
+ if (!e?.id || !e.caller?.id || !e.caller.name || e.mode !== "voice" && e.mode !== "video")
1653
+ throw new Error("Incoming call payload is invalid");
1654
+ if (e.expiresAt && Date.parse(e.expiresAt) <= Date.now()) {
1655
+ this.emit("callexpired", { id: e.id, expiresAt: e.expiresAt });
1656
+ return;
1657
+ }
1658
+ if (this.currentCall?.id === e.id) {
1659
+ this.currentCall = e, this.emit("callrestored", e);
1660
+ return;
1661
+ }
1662
+ if (this.seenInvitationIds.has(e.id)) {
1663
+ this.emit("callrestored", e);
1664
+ return;
1665
+ }
1666
+ this.seenInvitationIds.add(e.id), this.activeCallIds.add(e.id), this.currentCall = e, this.callAudio.startIncoming(), this.renderIncoming(e), this.emit("incomingcall", e);
1667
+ }
1668
+ /** Applies a call state received through the application's own push or signaling channel. */
1669
+ handleCallUpdate(e) {
1670
+ const t = e.id === this.currentCall?.id || e.id === this.outgoingCallId;
1671
+ t && e.status === "accepted" ? (this.activeCallIds.add(e.id), this.callAudio.stop(), this.dismiss(e.id)) : t && ["rejected", "busy", "cancelled", "ended", "failed", "missed"].includes(e.status) && this.showTerminal(e.id, Se(e.status)), ["rejected", "busy", "cancelled", "ended", "failed", "missed"].includes(e.status) && this.releaseMedia(e.id), this.emit("callupdated", e);
1672
+ }
1673
+ disconnect() {
1674
+ this.stopped = !0, this.reconnectTimer && clearTimeout(this.reconnectTimer), this.heartbeatTimer && clearInterval(this.heartbeatTimer), this.tokenRefreshTimer && clearTimeout(this.tokenRefreshTimer), this.reconnectTimer = null, this.heartbeatTimer = null, this.tokenRefreshTimer = null, this.socket?.close(1e3, "Client disconnected"), this.socket = null, this.callAudio.stop(), this.dismiss();
1675
+ for (const e of this.mediaClients.keys()) this.releaseMedia(e);
1676
+ this.activeCallIds.clear(), this.seenInvitationIds.clear();
1677
+ }
1678
+ destroy() {
1679
+ this.disconnect(), this.mount = null;
1680
+ }
1681
+ async openSocket(e) {
1682
+ if (!this.options.signalingUrl || this.stopped || this.socket?.readyState === WebSocket.OPEN || this.socket?.readyState === WebSocket.CONNECTING) return;
1683
+ let t;
1684
+ try {
1685
+ t = await this.resolveToken(e, e === "reconnect");
1686
+ } catch (s) {
1687
+ this.emit("tokenerror", x(s)), !this.stopped && this.options.reconnect !== !1 && this.scheduleReconnect();
1688
+ return;
1689
+ }
1690
+ if (this.stopped || this.socket) return;
1691
+ const i = new URL(this.options.signalingUrl, globalThis.location?.href);
1692
+ i.protocol = i.protocol === "https:" ? "wss:" : "ws:";
1693
+ const n = new WebSocket(i, ["rtc-client", t]);
1694
+ this.socket = n, n.addEventListener("open", () => {
1695
+ const s = this.reconnectAttempt > 0, a = this.lastDisconnect;
1696
+ this.reconnectAttempt = 0, this.heartbeatDeadline = Date.now() + 45e3, this.emit("signalingconnected", { resumed: s }), a && (this.reportSignalingDiagnostic({ event: "recovered", code: a.code, reason: a.reason, wasClean: a.wasClean, outageMs: Date.now() - a.at }), this.lastDisconnect = null), this.heartbeatTimer && clearInterval(this.heartbeatTimer), this.heartbeatTimer = setInterval(() => {
1697
+ if (n.readyState === WebSocket.OPEN) {
1698
+ if (Date.now() > this.heartbeatDeadline) {
1699
+ n.close(4e3, "Signaling heartbeat timed out");
1700
+ return;
1701
+ }
1702
+ n.send("ping");
1703
+ }
1704
+ }, 2e4);
1705
+ }), n.addEventListener("message", (s) => {
1706
+ this.heartbeatDeadline = Date.now() + 45e3, this.handleSignal(String(s.data));
1707
+ }), n.addEventListener("close", (s) => {
1708
+ if (this.socket !== n) return;
1709
+ this.socket = null, this.heartbeatTimer && clearInterval(this.heartbeatTimer), this.heartbeatTimer = null;
1710
+ const a = !this.stopped && this.options.reconnect !== !1, o = {
1711
+ code: s.code,
1712
+ reason: s.reason,
1713
+ wasClean: s.wasClean,
1714
+ willReconnect: a,
1715
+ attempt: this.reconnectAttempt + 1
1716
+ };
1717
+ a && (this.lastDisconnect = { code: s.code, reason: s.reason, wasClean: s.wasClean, at: Date.now() }), this.emit("signalingdisconnected", o), this.reportSignalingDiagnostic({ event: a ? "reconnecting" : "disconnected", ...o }), a && this.scheduleReconnect();
1718
+ }), n.addEventListener("error", () => {
1719
+ const s = { message: "RTC signaling connection failed", reconnecting: !this.stopped && this.options.reconnect !== !1, attempt: this.reconnectAttempt + 1 };
1720
+ this.emit("signalingerror", s), this.reportSignalingDiagnostic({ event: "error", attempt: s.attempt });
1721
+ });
1722
+ }
1723
+ handleSignal(e) {
1724
+ let t;
1725
+ try {
1726
+ t = JSON.parse(e);
1727
+ } catch {
1728
+ return;
1729
+ }
1730
+ const i = String(t.type ?? "");
1731
+ if (i === "call.incoming" && t.call && typeof t.call == "object") {
1732
+ this.handleIncomingCall(t.call);
1733
+ return;
1734
+ }
1735
+ if (i === "call.updated") {
1736
+ const n = t.call;
1737
+ n?.id && n.status && this.handleCallUpdate({ id: n.id, status: n.status });
1738
+ return;
1739
+ }
1740
+ i === "presence" ? this.emit("presence", { users: t.users ?? [] }) : i !== "pong" && this.emit(i || "signal", t);
1741
+ }
1742
+ async callAction(e, t) {
1743
+ return this.request(`/v1/rtc/client/calls/${encodeURIComponent(e)}/${t}`, {
1744
+ method: "POST",
1745
+ body: "{}"
1746
+ });
1747
+ }
1748
+ async request(e, t, i = !1) {
1749
+ const n = this.options.apiBaseUrl ?? this.options.signalingUrl ?? globalThis.location?.origin;
1750
+ if (!n) throw new Error("apiBaseUrl is required for call actions");
1751
+ const s = new URL(n, globalThis.location?.href), a = await this.resolveToken("initial"), o = new Headers(t.headers);
1752
+ o.set("authorization", `Bearer ${a}`), o.set("content-type", "application/json");
1753
+ const c = await fetch(new URL(e, s.origin), { ...t, headers: o }), d = await c.json().catch(() => ({}));
1754
+ if (c.status === 401 && this.options.tokenProvider && !i)
1755
+ return await this.resolveToken("unauthorized", !0), this.request(e, t, !0);
1756
+ if (!c.ok) {
1757
+ const l = d.error;
1758
+ throw new Error(typeof l == "string" ? l : l?.message || `RTC API failed with HTTP ${c.status}`);
1759
+ }
1760
+ return d;
1761
+ }
1762
+ renderIncoming(e) {
1763
+ if (!this.mount) return;
1764
+ this.clearUi();
1765
+ const t = document.createElement("section");
1766
+ t.dataset.rtcIncomingCall = e.id, t.setAttribute("role", "dialog"), t.setAttribute("aria-label", `Incoming ${e.mode} call from ${e.caller.name}`), Object.assign(t.style, {
1767
+ display: "grid",
1768
+ gap: "14px",
1769
+ padding: "20px",
1770
+ border: "1px solid #cbd8dc",
1771
+ borderRadius: "8px",
1772
+ background: "#fff",
1773
+ color: "#102027",
1774
+ boxShadow: "0 14px 38px #07101429",
1775
+ fontFamily: "system-ui, sans-serif"
1776
+ });
1777
+ const i = document.createElement("small");
1778
+ i.textContent = e.mode === "video" ? "Incoming video call" : "Incoming voice call", i.style.color = "#5d7079";
1779
+ const n = document.createElement("strong");
1780
+ n.textContent = e.caller.name, n.style.fontSize = "20px";
1781
+ const s = document.createElement("div");
1782
+ Object.assign(s.style, { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px" });
1783
+ const a = Y("Decline", "#fff", "#a32920", "#d9a09b"), o = Y("Answer", "#fff", "#08745f", "#08745f");
1784
+ a.addEventListener("click", () => {
1785
+ this.reject(e.id).catch((c) => this.emit("error", x(c)));
1786
+ }), o.addEventListener("click", () => {
1787
+ this.accept(e.id).catch((c) => this.emit("error", x(c)));
1788
+ }), s.appendChild(a), s.appendChild(o), t.appendChild(i), t.appendChild(n), t.appendChild(s), this.mount.replaceChildren(t);
1789
+ }
1790
+ dismiss(e) {
1791
+ e && this.currentCall?.id !== e && this.outgoingCallId !== e || (this.currentCall = null, (!e || this.outgoingCallId === e) && (this.outgoingCallId = null), this.callAudio.stop(), this.clearUi());
1792
+ }
1793
+ showTerminal(e, t) {
1794
+ if (this.callAudio.stop(), this.currentCall?.id === e && (this.currentCall = null), this.outgoingCallId === e && (this.outgoingCallId = null), !this.mount) return;
1795
+ this.clearUi();
1796
+ const i = document.createElement("section");
1797
+ i.dataset.rtcCallTerminal = e, i.setAttribute("role", "status"), Object.assign(i.style, {
1798
+ display: "grid",
1799
+ placeItems: "center",
1800
+ gap: "10px",
1801
+ minHeight: "150px",
1802
+ padding: "20px",
1803
+ border: "1px solid #cbd8dc",
1804
+ borderRadius: "8px",
1805
+ background: "#fff",
1806
+ color: "#102027",
1807
+ boxShadow: "0 14px 38px #07101429",
1808
+ fontFamily: "system-ui, sans-serif"
1809
+ });
1810
+ const n = document.createElement("strong");
1811
+ n.textContent = t, n.style.fontSize = "18px", i.appendChild(n), this.mount.replaceChildren(i);
1812
+ const s = Math.max(0, this.options.terminalStateDurationMs ?? 1600);
1813
+ this.terminalTimer = setTimeout(() => this.clearUi(), s);
1814
+ }
1815
+ clearUi() {
1816
+ this.terminalTimer && clearTimeout(this.terminalTimer), this.terminalTimer = null, this.mount && this.mount.replaceChildren();
1817
+ }
1818
+ scheduleReconnect() {
1819
+ this.reconnectTimer && clearTimeout(this.reconnectTimer);
1820
+ const e = Math.min(3e4, 1e3 * 2 ** Math.min(this.reconnectAttempt, 5)) + Math.round(Math.random() * 300);
1821
+ this.reconnectAttempt += 1, this.reconnectTimer = setTimeout(() => {
1822
+ this.openSocket("reconnect");
1823
+ }, e);
1824
+ }
1825
+ async resolveToken(e, t = !1) {
1826
+ if (!t && this.currentToken && !this.tokenNeedsRefresh()) return this.currentToken;
1827
+ if (!this.options.tokenProvider) {
1828
+ if (!this.currentToken) throw new Error("A valid RTC client token is required");
1829
+ return this.currentToken;
1830
+ }
1831
+ return this.tokenRequest ? this.tokenRequest : (this.tokenRequest = (async () => {
1832
+ const i = await this.options.tokenProvider?.({ reason: e, previousExpiresAt: this.currentTokenExpiresAt }), n = typeof i == "string" ? { token: i } : i;
1833
+ if (!n?.token?.startsWith("rtcc_")) throw new Error("tokenProvider returned an invalid RTC client token");
1834
+ return this.currentToken = n.token, this.currentTokenExpiresAt = n.expiresAt, this.scheduleTokenRefresh(), this.emit("tokenrefreshed", { reason: e, expiresAt: n.expiresAt }), n.token;
1835
+ })().finally(() => {
1836
+ this.tokenRequest = null;
1837
+ }), this.tokenRequest);
1838
+ }
1839
+ tokenNeedsRefresh() {
1840
+ if (!this.currentTokenExpiresAt) return !1;
1841
+ const e = Date.parse(this.currentTokenExpiresAt) - Date.now();
1842
+ return Number.isFinite(e) ? e <= this.refreshSkew(e) : !0;
1843
+ }
1844
+ refreshSkew(e) {
1845
+ const t = Math.max(5e3, this.options.tokenRefreshSkewMs ?? 3e5);
1846
+ return Math.min(t, Math.max(5e3, e * 0.2));
1847
+ }
1848
+ scheduleTokenRefresh() {
1849
+ if (this.tokenRefreshTimer && clearTimeout(this.tokenRefreshTimer), this.tokenRefreshTimer = null, this.stopped || !this.options.tokenProvider || !this.currentTokenExpiresAt) return;
1850
+ const e = Date.parse(this.currentTokenExpiresAt) - Date.now();
1851
+ if (!Number.isFinite(e)) return;
1852
+ const t = Math.max(0, e - this.refreshSkew(e));
1853
+ this.tokenRefreshTimer = setTimeout(() => {
1854
+ this.tokenRefreshTimer = null, this.resolveToken("expiring", !0).catch((i) => {
1855
+ this.emit("tokenerror", x(i)), this.stopped || (this.tokenRefreshTimer = setTimeout(() => this.scheduleTokenRefresh(), 3e4));
1856
+ });
1857
+ }, Math.min(t, 2147e6));
1858
+ }
1859
+ releaseMedia(e) {
1860
+ this.activeCallIds.delete(e), this.seenInvitationIds.delete(e);
1861
+ const t = this.mediaClients.get(e);
1862
+ t && (this.mediaClients.delete(e), t.leave().catch(() => {
1863
+ }).finally(() => t.destroy()));
1864
+ }
1865
+ reportSignalingDiagnostic(e) {
1866
+ this.options.reportSignalingDiagnostics === !1 || this.stopped || this.request("/v1/rtc/client/signaling-events", {
1867
+ method: "POST",
1868
+ body: JSON.stringify({ ...e, occurredAt: (/* @__PURE__ */ new Date()).toISOString() })
1869
+ }).catch((t) => this.emit("diagnosticserror", x(t)));
1870
+ }
1871
+ emit(e, t) {
1872
+ const i = { type: e, detail: t, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
1873
+ this.dispatchEvent(new CustomEvent(e, { detail: i })), this.options.onEvent?.(i);
1874
+ }
1875
+ }
1876
+ function Ae(r) {
1877
+ const e = new ye(r);
1878
+ return r.autoConnect !== !1 && r.signalingUrl && e.connect(), e;
1879
+ }
1880
+ function K(r) {
1881
+ return r ? typeof r == "string" ? document.querySelector(r) : r : null;
1882
+ }
1883
+ function Y(r, e, t, i) {
1884
+ const n = document.createElement("button");
1885
+ return n.type = "button", n.textContent = r, Object.assign(n.style, {
1886
+ minHeight: "44px",
1887
+ border: `1px solid ${i}`,
1888
+ borderRadius: "6px",
1889
+ background: t,
1890
+ color: e,
1891
+ cursor: "pointer",
1892
+ font: "inherit",
1893
+ fontWeight: "700"
1894
+ }), n;
1895
+ }
1896
+ function Se(r) {
1897
+ return {
1898
+ rejected: "Call declined",
1899
+ busy: "Line busy",
1900
+ cancelled: "Call cancelled",
1901
+ ended: "Call ended",
1902
+ failed: "Call failed",
1903
+ missed: "No answer"
1904
+ }[r] ?? "Call ended";
1905
+ }
1906
+ function X(r) {
1907
+ return {
1908
+ id: r.participantId,
1909
+ name: r.displayName,
1910
+ role: "participant"
1911
+ };
1912
+ }
1913
+ async function ke(r, e) {
1914
+ const t = await r.getStats(), i = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
1915
+ let s = 0, a = 0, o = 0, c = 0, d = 0, l = 0, h = 0, v = 0, g = 0, k = 0, p = 0, C = 0, S = 0, y = null, T = null, I = null, R = null, E = null, f = null;
1916
+ t.forEach((m) => {
1917
+ n.set(m.id, m), m.type === "codec" && i.set(m.id, String(m.mimeType ?? ""));
1918
+ }), t.forEach((m) => {
1919
+ if (m.type === "inbound-rtp" && !m.isRemote && (s += Number(m.bytesReceived ?? 0), o += Math.max(0, Number(m.packetsLost ?? 0)), c += Math.max(0, Number(m.packetsReceived ?? 0)), Number.isFinite(m.jitter) && (d += Number(m.jitter), l += 1), h = Math.max(h, Number(m.framesPerSecond ?? 0)), v = Math.max(v, Number(m.frameWidth ?? 0)), g = Math.max(g, Number(m.frameHeight ?? 0)), k += Number(m.framesDropped ?? 0), p += Number(m.freezeCount ?? 0), C += Number(m.totalFreezesDuration ?? 0), S += Number(m.concealedSamples ?? 0), E ||= i.get(String(m.codecId ?? "")) ?? null), m.type === "outbound-rtp" && !m.isRemote && (a += Number(m.bytesSent ?? 0), f ||= m.qualityLimitationReason ? String(m.qualityLimitationReason) : null, E ||= i.get(String(m.codecId ?? "")) ?? null), m.type === "candidate-pair" && (m.selected || m.nominated) && m.state === "succeeded") {
1920
+ y = Number.isFinite(m.currentRoundTripTime) ? Number(m.currentRoundTripTime) : y, T = Number.isFinite(m.availableOutgoingBitrate) ? Number(m.availableOutgoingBitrate) : T;
1921
+ const A = n.get(m.localCandidateId);
1922
+ I = A?.candidateType ? String(A.candidateType) : I, R = A?.protocol ? String(A.protocol) : R;
1923
+ }
1924
+ });
1925
+ const u = Date.now(), q = e ? Math.max(1e-3, (u - e.timestamp) / 1e3) : 0, L = e ? Math.max(0, Math.round((s - e.inboundBytes) * 8 / q)) : null, $ = e ? Math.max(0, Math.round((a - e.outboundBytes) * 8 / q)) : null, b = o + c, P = e ? Math.max(0, o - e.packetsLost) : o, w = e ? Math.max(0, c - e.packetsReceived) : c, N = P + w;
1926
+ return {
1927
+ counters: { timestamp: u, inboundBytes: s, outboundBytes: a, packetsLost: o, packetsReceived: c },
1928
+ sample: {
1929
+ rttMs: y === null ? null : y * 1e3,
1930
+ jitterMs: l ? d / l * 1e3 : null,
1931
+ packetLossPct: N ? P / N * 100 : null,
1932
+ packetLossCumulativePct: b ? o / b * 100 : null,
1933
+ packetsLost: o,
1934
+ packetsReceived: c,
1935
+ inboundBitrateBps: L,
1936
+ outboundBitrateBps: $,
1937
+ availableOutgoingBitrateBps: T,
1938
+ framesPerSecond: h || null,
1939
+ frameWidth: v || null,
1940
+ frameHeight: g || null,
1941
+ framesDropped: k,
1942
+ freezeCount: p,
1943
+ freezeDurationMs: Math.round(C * 1e3),
1944
+ concealedSamples: S,
1945
+ codec: E,
1946
+ transportProtocol: R,
1947
+ candidateType: I,
1948
+ qualityLimitationReason: f
1949
+ }
1950
+ };
1951
+ }
1952
+ function Te(r) {
1953
+ let e = r.mediaScore == null ? 5 : Math.round(ee(Number(r.mediaScore), 0, 5));
1954
+ const t = F(r.packetLossPct) ?? 0, i = F(r.rttMs) ?? 0, n = F(r.jitterMs) ?? 0, s = String(r.connectionState ?? "unknown").toLowerCase();
1955
+ ["failed", "closed", "disconnected"].includes(s) ? e = 0 : ["reconnecting", "checking"].includes(s) && (e = Math.min(e, 2)), t >= 15 ? e = Math.min(e, 0) : t >= 8 ? e = Math.min(e, 1) : t >= 3 ? e = Math.min(e, 2) : t >= 1 && (e = Math.min(e, 3)), i >= 1200 ? e = Math.min(e, 0) : i >= 800 ? e = Math.min(e, 1) : i >= 400 ? e = Math.min(e, 2) : i >= 250 && (e = Math.min(e, 3)), n >= 150 ? e = Math.min(e, 1) : n >= 80 ? e = Math.min(e, 2) : n >= 40 && (e = Math.min(e, 3));
1956
+ const a = Number(ee(4.5 - t * 0.09 - i * 11e-4 - n * 6e-3, 1, 4.5).toFixed(2));
1957
+ return { networkScore: e, estimatedMos: a };
1958
+ }
1959
+ function Z() {
1960
+ return navigator.connection ?? null;
1961
+ }
1962
+ function we() {
1963
+ const r = Number(navigator.deviceMemory);
1964
+ return Number.isFinite(r) ? r : null;
1965
+ }
1966
+ function F(r) {
1967
+ const e = Number(r);
1968
+ return Number.isFinite(e) ? e : null;
1969
+ }
1970
+ function ee(r, e, t) {
1971
+ return Math.min(t, Math.max(e, r));
1972
+ }
1973
+ function x(r) {
1974
+ return r instanceof Error ? { name: r.name, message: r.message } : { name: "ServiceError", message: String(r ?? "Service request failed") };
1975
+ }
1976
+ function te(r, e) {
1977
+ return r instanceof Error ? r : new Error(e);
1978
+ }
1979
+ const be = "1.3.15";
1980
+ export {
1981
+ me as BACKGROUND_PRESETS,
1982
+ pe as OVERLAY_EFFECTS,
1983
+ fe as RtcClient,
1984
+ ye as RtcIncomingClient,
1985
+ Ce as SPEAKER_NOISE_FLOOR,
1986
+ Ie as SPEAKER_TAKEOVER_RATIO,
1987
+ Re as createCallRecorder,
1988
+ Ee as createPictureInPictureController,
1989
+ Me as createRtcClient,
1990
+ Ae as createRtcIncomingClient,
1991
+ Pe as createVideoEffectsPipeline,
1992
+ ie as pickActiveSpeaker,
1993
+ G as scenePainters,
1994
+ ge as unlockRtcCallAudio,
1995
+ be as version
1996
+ };