@streaming-cdn/rtc-web 1.3.12 → 1.3.14

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,1802 @@
1
+ const fe = 0.02, ge = 1.4;
2
+ function H(c, e, t = 0.02, i = 1.4) {
3
+ const n = c.filter((r) => r.level >= t);
4
+ if (n.length === 0) return null;
5
+ const s = n.reduce((r, o) => o.level > r.level ? o : r), a = n.find((r) => r.participantId === e);
6
+ return a ? s.participantId === e ? e : s.level >= a.level * i ? s.participantId : e : s.participantId;
7
+ }
8
+ function _(c) {
9
+ const e = [];
10
+ for (const t of c) {
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 f = {
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
+ }, X = {
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 Z(c) {
35
+ const e = String(c ?? "").trim().toLowerCase();
36
+ return !e || e === "none" ? "" : e === "cpu" || e === "bandwidth" ? e : "other";
37
+ }
38
+ function ee(c, e, t, i, n = {}) {
39
+ const s = { ...X, ...n.thresholds }, a = ["low", "medium", "high"], r = n.ceiling && n.ceiling !== "audio" ? n.ceiling : "high", o = a.indexOf(r), l = a.indexOf(c), d = (n.samplesSeen ?? s.warmupSamples) + 1, u = d <= s.warmupSamples, m = Z(e.qualityLimitationReason), p = e.packetLossPct ?? 0, v = e.rttMs ?? 0, T = e.availableOutgoingBitrateBps, E = f[c].maxBitrate, w = p >= s.criticalLossPct || v >= s.criticalRttMs || m === "cpu", y = w || p >= s.poorLossPct || v >= s.poorRttMs || m === "bandwidth", b = T == null || T >= E * s.sustainRatio, C = p < s.goodLossPct && v < s.goodRttMs && !m && b, S = y ? t + 1 : 0, k = C ? i + 1 : 0, I = { quality: c, poorSamples: S, goodSamples: k, samplesSeen: d, changed: !1 };
40
+ return l > o ? { quality: a[o], poorSamples: 0, goodSamples: 0, samplesSeen: d, changed: !0 } : u ? I : w && l > 0 ? { quality: a[l - 1], poorSamples: 0, goodSamples: 0, samplesSeen: d, changed: !0 } : S >= s.poorSamplesToStepDown && l > 0 ? { quality: a[l - 1], poorSamples: 0, goodSamples: 0, samplesSeen: d, changed: !0 } : k >= s.goodSamplesToStepUp && l < o ? { quality: a[l + 1], poorSamples: 0, goodSamples: 0, samplesSeen: d, changed: !0 } : I;
41
+ }
42
+ function te(c, e) {
43
+ const t = c instanceof Map ? c : new Map(c);
44
+ let i = 0, n = 0, s = null, a = null, r = null;
45
+ for (const d of t.values()) {
46
+ const u = String(d.type ?? "");
47
+ if (u === "outbound-rtp" && String(d.kind ?? "") === "video" && (i += Number(d.packetsSent ?? 0), s = d.qualityLimitationReason ?? s), u === "remote-inbound-rtp" && String(d.kind ?? "") === "video") {
48
+ n += Number(d.packetsLost ?? 0);
49
+ const m = Number(d.roundTripTime ?? NaN);
50
+ Number.isFinite(m) && (a = m * 1e3);
51
+ }
52
+ if (u === "candidate-pair" && (d.nominated === !0 || String(d.state ?? "") === "succeeded")) {
53
+ const m = Number(d.currentRoundTripTime ?? NaN);
54
+ a == null && Number.isFinite(m) && (a = m * 1e3);
55
+ const p = Number(d.availableOutgoingBitrate ?? NaN);
56
+ Number.isFinite(p) && (r = p);
57
+ }
58
+ }
59
+ const o = { packetsSent: i, packetsLost: n, timestamp: Date.now() };
60
+ let l = null;
61
+ if (e) {
62
+ const d = i - e.packetsSent, u = n - e.packetsLost;
63
+ d > 0 && u >= 0 && (l = u / (d + u) * 100);
64
+ }
65
+ return {
66
+ sample: { packetLossPct: l, rttMs: a, availableOutgoingBitrateBps: r, qualityLimitationReason: s },
67
+ counters: o
68
+ };
69
+ }
70
+ const ie = {
71
+ audio: { active: !1, maxBitrate: 0, maxFramerate: 0, scaleResolutionDownBy: 4 }
72
+ }, ne = { active: !0, maxBitrate: 4e6, maxFramerate: 30, scaleResolutionDownBy: 1 };
73
+ class A {
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((r) => r.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((r) => r.readyState === "live") || await this.replaceVideoTrack(n);
159
+ return;
160
+ }
161
+ for (const r of this.peers.values()) {
162
+ const o = r.connection.getSenders().find((l) => l.track?.kind === s);
163
+ o ? await o.replaceTrack(n) : r.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 = A.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((o, l) => {
218
+ this.resolveJoin = o, this.rejectJoin = l;
219
+ }), s = new WebSocket(i, ["rtc-client", t.signalingToken]), a = ++this.socketGeneration;
220
+ this.socket = s;
221
+ const r = 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", (o) => {
225
+ const l = String(o.data);
226
+ this.signalQueue = this.signalQueue.then(async () => {
227
+ this.socket !== s || this.socketGeneration !== a || await this.handleSignal(l);
228
+ }).catch((d) => {
229
+ const u = d instanceof Error ? d.message : "RTC signaling processing failed";
230
+ this.options.onEvent("signalingerror", { message: u }), this.joined || this.rejectJoin?.(new Error(u));
231
+ });
232
+ }), s.addEventListener("error", () => this.rejectJoin?.(new Error("RTC signaling connection failed"))), s.addEventListener("close", (o) => {
233
+ if (this.socket !== s) return;
234
+ this.socket = null, this.stopHeartbeat(), this.stopQualitySampling(), !this.joined && !e && this.rejectJoin?.(new Error(o.reason || "RTC signaling connection closed"));
235
+ const l = !this.stopped && (this.joined || e);
236
+ this.joined = !1, l ? (this.resetPeers(), this.options.onEvent("reconnecting", { reason: o.reason, code: o.code, attempt: this.reconnectAttempt + 1 }), this.scheduleReconnect()) : this.stopped || this.options.onEvent("disconnected", { reason: o.reason, code: o.code });
237
+ });
238
+ try {
239
+ await n, this.joined = !0, this.options.onEvent("connected", { participantId: t.participantId, resumed: e });
240
+ } finally {
241
+ clearTimeout(r), 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 r = await n.connection.createAnswer();
277
+ await n.connection.setLocalDescription(r), 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: A.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 o of this.localStream?.getAudioTracks() ?? []) i.addTrack(o, this.localStream);
324
+ const a = this.screenStream?.getVideoTracks().some((o) => o.readyState === "live") ? this.screenStream : this.localStream, r = a?.getVideoTracks()[0];
325
+ return r && a && (s.videoSender = i.addTrack(r, a)), !this.localStream?.getAudioTracks().length && this.options.receiveAudio && i.addTransceiver("audio", { direction: "recvonly" }), !r && this.options.receiveVideo && (s.videoTransceiver = i.addTransceiver("video", { direction: "recvonly" }), s.videoSender = s.videoTransceiver.sender), this.applyCodecPreference(i), this.applyVideoQuality(i), i.addEventListener("icecandidate", (o) => {
326
+ o.candidate && this.send({ type: "rtc.ice", targetParticipantId: e.participantId, candidate: o.candidate.toJSON() });
327
+ }), i.addEventListener("track", (o) => {
328
+ n.getTracks().some((l) => l.id === o.track.id) || n.addTrack(o.track), this.options.onEvent("remotestream", { participant: e, stream: n, track: o.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 = L();
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 ? ne : this.quality === "audio" ? ie.audio : f[e.quality], a = i.getParameters(), r = a.encodings?.length ? a.encodings : [{}];
395
+ a.encodings = r.map((o) => ({ ...o, ...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 _(n.values()))
415
+ e.push({ participantId: i.identity.participantId, level: s.level });
416
+ } catch {
417
+ }
418
+ const t = H(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 } = te(t, e.counters);
432
+ e.counters = n;
433
+ const s = ee(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 ?? L(),
491
+ ...e
492
+ }));
493
+ }
494
+ stopStream(e) {
495
+ for (const t of e?.getTracks() ?? []) t.stop();
496
+ }
497
+ }
498
+ function L() {
499
+ return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
500
+ }
501
+ const M = { active: !0, maxBitrate: 4e6, maxFramerate: 30, scaleResolutionDownBy: 1 }, j = [
502
+ { rid: "a", maxBitrate: f.high.maxBitrate, maxFramerate: f.high.maxFramerate },
503
+ { rid: "b", maxBitrate: f.medium.maxBitrate, maxFramerate: f.medium.maxFramerate, scaleResolutionDownBy: 2 },
504
+ { rid: "c", maxBitrate: f.low.maxBitrate, maxFramerate: f.low.maxFramerate, scaleResolutionDownBy: 4 }
505
+ ], O = { high: "a", medium: "b", low: "c" };
506
+ class se {
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((o) => o.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((o) => o.readyState === "live") || await this.replaceVideoTrack(n);
600
+ return;
601
+ }
602
+ const r = this.connection?.getSenders().find((o) => o.track?.kind === "audio");
603
+ r && await r.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((o, l) => {
642
+ this.resolveJoin = o, this.rejectJoin = l;
643
+ }), s = new WebSocket(i, ["rtc-client", t.signalingToken]), a = ++this.socketGeneration;
644
+ this.socket = s;
645
+ const r = 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", (o) => {
649
+ const l = String(o.data);
650
+ this.signalQueue = this.signalQueue.then(async () => {
651
+ this.socket !== s || this.socketGeneration !== a || await this.handleSignal(l);
652
+ }).catch((d) => {
653
+ const u = d instanceof Error ? d.message : "RTC signaling processing failed";
654
+ this.options.onEvent("signalingerror", { message: u }), this.joined || this.rejectJoin?.(new Error(u));
655
+ });
656
+ }), s.addEventListener("error", () => this.rejectJoin?.(new Error("RTC signaling connection failed"))), s.addEventListener("close", (o) => {
657
+ if (this.socket !== s) return;
658
+ this.socket = null, this.stopHeartbeat(), !this.joined && !e && this.rejectJoin?.(new Error(o.reason || "RTC signaling connection closed"));
659
+ const l = !this.stopped && (this.joined || e);
660
+ this.joined = !1, l ? (this.options.onEvent("reconnecting", { reason: o.reason, code: o.code, attempt: this.reconnectAttempt + 1 }), this.scheduleReconnect()) : this.stopped || this.options.onEvent("disconnected", { reason: o.reason, code: o.code });
661
+ });
662
+ try {
663
+ await n, this.joined = !0, this.options.onEvent("connected", { participantId: t.participantId, resumed: e });
664
+ } finally {
665
+ clearTimeout(r), 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", (r) => this.handleRemoteTrack(r)), 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((r) => t.addTransceiver(r, {
719
+ direction: "sendonly",
720
+ streams: this.localStream ? [this.localStream] : [],
721
+ ...r.kind === "video" ? { sendEncodings: j.map((o) => ({ ...o })) } : {}
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((r) => ({
730
+ location: "local",
731
+ mid: r.mid,
732
+ trackName: this.trackName(r.sender.track)
733
+ }))
734
+ }
735
+ );
736
+ a.sessionDescription && await t.setRemoteDescription(a.sessionDescription), this.videoSender = t.getSenders().find((r) => r.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((r) => ({ trackName: String(r.trackName ?? ""), simulcast: r.simulcast === !0 })).filter((r) => !!r.trackName), s = n.map((r) => r.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 r of n)
766
+ r.simulcast && a.simulcastNames.add(r.trackName);
767
+ this.remotes.set(e.participantId, a), this.negotiationQueue = this.negotiationQueue.then(async () => {
768
+ const r = s.filter((o) => !a.trackNames.includes(o));
769
+ r.length !== 0 && await this.subscribe(a, r);
770
+ }).catch((r) => {
771
+ this.options.onEvent("signalingerror", { message: r instanceof Error ? r.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: O[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 : O[this.quality];
834
+ n.encodings = s.map((r) => {
835
+ if (i)
836
+ return { ...r, active: r.rid === "a", ...r.rid === "a" ? { maxBitrate: M.maxBitrate, maxFramerate: M.maxFramerate } : {} };
837
+ const o = j.find((d) => d.rid === r.rid), l = a != null && String(r.rid ?? "") >= a;
838
+ return { ...r, ...o ?? {}, active: l };
839
+ });
840
+ } else {
841
+ const a = this.quality === "audio" ? f.audio : f[this.quality], r = i ? M : a;
842
+ n.encodings = s.map((o) => ({ ...o, ...r }));
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 _(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 = H(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 ?? ae(),
930
+ ...e
931
+ }));
932
+ }
933
+ stopStream(e) {
934
+ for (const t of e?.getTracks() ?? []) t.stop();
935
+ }
936
+ }
937
+ function ae() {
938
+ return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
939
+ }
940
+ function ve(c, 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(r) {
947
+ const o = c.getRemoteStreams();
948
+ return r ? o.find((l) => l.participant.participantId === r)?.stream ?? null : o[0]?.stream ?? c.getLocalStream();
949
+ }
950
+ return {
951
+ supported: i,
952
+ async enter(r) {
953
+ if (!i) throw new Error("This browser does not support picture-in-picture");
954
+ const o = a(r);
955
+ if (!o) throw new Error("No video stream is available for picture-in-picture");
956
+ const l = s();
957
+ l.srcObject !== o && (l.srcObject = o), await l.play().catch(() => {
958
+ }), await l.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
+ class re extends EventTarget {
969
+ mode;
970
+ options;
971
+ client = null;
972
+ peerConnection;
973
+ state = "initialized";
974
+ latestMediaScore = null;
975
+ latestQuality = null;
976
+ statsTimer = null;
977
+ statsRunning = !1;
978
+ previousCounters = null;
979
+ lastTelemetryAt = 0;
980
+ joinPromise = null;
981
+ requestedQuality;
982
+ effectiveQuality;
983
+ poorQualitySamples = 0;
984
+ goodQualitySamples = 0;
985
+ constructor(e) {
986
+ if (super(), !e.credential?.signalingToken || !e.credential.signalingUrl || !e.credential.roomId || !e.credential.participantId)
987
+ throw new Error("A valid room signaling credential is required");
988
+ 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 = {
989
+ ...e,
990
+ audio: e.audio ?? !0,
991
+ video: e.mode === "voice" ? !1 : e.video ?? !0,
992
+ receiveAudio: e.receiveAudio ?? !0,
993
+ receiveVideo: e.mode === "voice" ? !1 : e.receiveVideo ?? !0,
994
+ statsInterval: Math.max(1e3, e.statsInterval ?? 5e3),
995
+ telemetryInterval: Math.max(1e4, e.telemetryInterval ?? 3e4),
996
+ autoReportStats: e.autoReportStats ?? !0
997
+ };
998
+ }
999
+ async initialize() {
1000
+ if (this.client) return this;
1001
+ try {
1002
+ const t = (this.options.transport === "auto" ? this.mode === "meeting" ? "sfu" : "mesh" : this.options.transport ?? "mesh") === "sfu" ? se : A;
1003
+ return this.client = new t({
1004
+ credential: {
1005
+ signalingToken: this.options.credential.signalingToken,
1006
+ signalingUrl: this.options.credential.signalingUrl,
1007
+ roomId: this.options.credential.roomId,
1008
+ participantId: this.options.credential.participantId,
1009
+ externalUserId: this.options.credential.externalUserId,
1010
+ iceServers: this.options.credential.iceServers
1011
+ },
1012
+ audio: this.options.audio,
1013
+ video: this.options.video,
1014
+ receiveAudio: this.options.receiveAudio,
1015
+ receiveVideo: this.options.receiveVideo,
1016
+ quality: this.effectiveQuality,
1017
+ preferredVideoCodec: this.options.preferredVideoCodec ?? "auto",
1018
+ iceTransportPolicy: this.options.iceTransportPolicy ?? "all",
1019
+ localStream: this.options.localStream,
1020
+ onEvent: (i, n) => this.handleNativeEvent(i, n)
1021
+ }), await this.client.initialize(), this.startStats(), this.emit("initialized", { mode: this.mode, sdkVersion: pe }), this.options.autoJoin && await this.join(), this;
1022
+ } catch (e) {
1023
+ this.state = "failed";
1024
+ const t = z(e, "RTC initialization failed");
1025
+ throw this.emit("error", g(t)), t;
1026
+ }
1027
+ }
1028
+ async join() {
1029
+ const e = this.requireClient();
1030
+ return this.joinPromise ? this.joinPromise : (this.state = "joining", this.emit("joining", {}), this.joinPromise = e.join().catch((t) => {
1031
+ this.state = "failed";
1032
+ const i = z(t, "RTC connection failed");
1033
+ throw this.emit("error", g(i)), i;
1034
+ }).finally(() => {
1035
+ this.joinPromise = null;
1036
+ }), this.joinPromise);
1037
+ }
1038
+ async leave() {
1039
+ this.client && (await this.joinPromise?.catch(() => {
1040
+ }), await this.sampleAndReport("final").catch(() => {
1041
+ }), await this.client.leave(), this.state = "disconnected", this.stopStats());
1042
+ }
1043
+ async setMuted(e) {
1044
+ this.requireClient().setMuted(e), this.emit("mediachange", { kind: "audio", enabled: !e });
1045
+ }
1046
+ async setCameraEnabled(e) {
1047
+ if (this.mode === "voice" && e) throw new Error("Camera is unavailable in voice mode");
1048
+ await this.requireClient().setCameraEnabled(e), this.emit("mediachange", { kind: "video", enabled: e });
1049
+ }
1050
+ async setVideoTrack(e) {
1051
+ if (this.mode === "voice") throw new Error("Camera is unavailable in voice mode");
1052
+ await this.requireClient().setVideoTrack(e), this.emit("mediachange", { kind: "video", enabled: e.enabled, trackId: e.id });
1053
+ }
1054
+ async setScreenShareEnabled(e) {
1055
+ await this.requireClient().setScreenShareEnabled(e), this.emit("mediachange", { kind: "screenshare", enabled: e });
1056
+ }
1057
+ async setDevice(e) {
1058
+ await this.requireClient().setDevice(e), this.emit("devicechange", { deviceId: e.deviceId, kind: e.kind });
1059
+ }
1060
+ async setQuality(e) {
1061
+ if (this.mode === "voice" && e !== "audio") throw new Error("Voice calls only support audio quality");
1062
+ this.requestedQuality = e, this.poorQualitySamples = 0, this.goodQualitySamples = 0, await this.applyEffectiveQuality(e === "auto" ? "medium" : e, "manual");
1063
+ }
1064
+ async setPreferredVideoCodec(e) {
1065
+ await this.requireClient().setPreferredVideoCodec(e), this.emit("codecchange", { preferredVideoCodec: e });
1066
+ }
1067
+ async getDevices() {
1068
+ return navigator.mediaDevices?.enumerateDevices ? navigator.mediaDevices.enumerateDevices() : [];
1069
+ }
1070
+ getParticipants() {
1071
+ return this.requireClient().getParticipants().map(F);
1072
+ }
1073
+ getLocalStream() {
1074
+ return this.client?.getLocalStream() ?? null;
1075
+ }
1076
+ getRemoteStreams() {
1077
+ return (this.client?.getRemoteStreams() ?? []).map((e) => ({
1078
+ participant: F(e.participant),
1079
+ stream: e.stream
1080
+ }));
1081
+ }
1082
+ setPeerConnection(e) {
1083
+ this.peerConnection = e, this.previousCounters = null;
1084
+ }
1085
+ async sendChatMessage(e, t) {
1086
+ const i = e.trim();
1087
+ if (!i) throw new Error("message is required");
1088
+ this.requireClient().sendChat(i, t);
1089
+ }
1090
+ async sendControlMessage(e, t = {}) {
1091
+ const i = e.trim();
1092
+ if (!i) throw new Error("action is required");
1093
+ this.requireClient().sendControl(i, t);
1094
+ }
1095
+ async createPoll(e, t, i = {}) {
1096
+ const n = t.map((s) => s.trim()).filter(Boolean);
1097
+ if (!e.trim() || n.length < 2) throw new Error("A poll requires a question and at least two options");
1098
+ this.requireClient().sendControl("poll.create", {
1099
+ poll: {
1100
+ id: `poll-${crypto.randomUUID()}`,
1101
+ question: e.trim(),
1102
+ options: n,
1103
+ votes: {},
1104
+ ...i
1105
+ }
1106
+ });
1107
+ }
1108
+ async votePoll(e, t) {
1109
+ this.requireClient().sendControl("poll.vote", { pollId: e, optionIndex: t });
1110
+ }
1111
+ async requestStageAccess() {
1112
+ this.requireClient().sendControl("stage.request");
1113
+ }
1114
+ async cancelStageAccess() {
1115
+ this.requireClient().sendControl("stage.cancel");
1116
+ }
1117
+ async joinStage() {
1118
+ this.requireClient().sendControl("stage.join");
1119
+ }
1120
+ async leaveStage() {
1121
+ this.requireClient().sendControl("stage.leave");
1122
+ }
1123
+ async grantStageAccess(e) {
1124
+ this.requireClient().sendControl("stage.grant", { participantIds: e });
1125
+ }
1126
+ async denyStageAccess(e) {
1127
+ this.requireClient().sendControl("stage.deny", { participantIds: e });
1128
+ }
1129
+ async startRecording(e = !1) {
1130
+ this.requireClient().sendControl("recording.start", { allowMultiple: e });
1131
+ }
1132
+ async stopRecording(e) {
1133
+ this.requireClient().sendControl("recording.stop", { recordingId: e });
1134
+ }
1135
+ async pauseRecording(e) {
1136
+ this.requireClient().sendControl("recording.pause", { recordingId: e });
1137
+ }
1138
+ async resumeRecording(e) {
1139
+ this.requireClient().sendControl("recording.resume", { recordingId: e });
1140
+ }
1141
+ async runPreflight(e = {}) {
1142
+ const t = [], i = globalThis.isSecureContext === !0;
1143
+ i || t.push("secure_context_required");
1144
+ let n = "unavailable";
1145
+ if (e.requestMedia !== !1 && navigator.mediaDevices?.getUserMedia)
1146
+ try {
1147
+ (await navigator.mediaDevices.getUserMedia({ audio: this.options.audio, video: this.options.video })).getTracks().forEach((m) => m.stop()), n = "granted";
1148
+ } catch {
1149
+ n = "denied", t.push("media_permission_denied");
1150
+ }
1151
+ const s = navigator.mediaDevices?.enumerateDevices ? await navigator.mediaDevices.enumerateDevices().catch(() => []) : [], a = s.filter((u) => u.kind === "audioinput").length, r = s.filter((u) => u.kind === "videoinput").length;
1152
+ this.options.audio && a === 0 && t.push("microphone_unavailable"), this.options.video && r === 0 && t.push("camera_unavailable");
1153
+ const o = e.probeUrl ?? this.options.preflightProbeUrl;
1154
+ let l = null;
1155
+ if (o) {
1156
+ const u = performance.now();
1157
+ try {
1158
+ const m = await fetch(o, { method: "HEAD", cache: "no-store", credentials: "omit" });
1159
+ l = Math.round(performance.now() - u), m.ok || t.push("network_probe_failed");
1160
+ } catch {
1161
+ t.push("network_probe_failed");
1162
+ }
1163
+ }
1164
+ const d = {
1165
+ passed: t.length === 0,
1166
+ sampledAt: (/* @__PURE__ */ new Date()).toISOString(),
1167
+ secureContext: i,
1168
+ mediaPermission: n,
1169
+ microphoneCount: a,
1170
+ cameraCount: r,
1171
+ probeLatencyMs: l,
1172
+ effectiveConnectionType: J()?.effectiveType ?? null,
1173
+ issues: t
1174
+ };
1175
+ return this.emit("preflight", d), this.options.autoReportStats && await this.reportQuality({
1176
+ sampleType: "preflight",
1177
+ sampledAt: d.sampledAt,
1178
+ connectionState: d.passed ? "ready" : "failed",
1179
+ rttMs: d.probeLatencyMs,
1180
+ effectiveConnectionType: d.effectiveConnectionType
1181
+ }).catch(() => {
1182
+ }), d;
1183
+ }
1184
+ async collectQualitySample(e = "runtime") {
1185
+ const t = this.options.statsProvider ? await this.options.statsProvider() : {}, i = this.peerConnection ? await he(this.peerConnection, this.previousCounters) : null;
1186
+ i?.counters && (this.previousCounters = i.counters);
1187
+ const s = { ...{
1188
+ sampleType: e,
1189
+ sampledAt: (/* @__PURE__ */ new Date()).toISOString(),
1190
+ connectionState: this.peerConnection?.connectionState ?? this.state,
1191
+ mediaScore: this.latestMediaScore,
1192
+ networkScore: this.latestMediaScore == null ? 5 : Math.max(0, Math.min(5, Math.round(this.latestMediaScore))),
1193
+ estimatedMos: 4.5,
1194
+ rttMs: null,
1195
+ jitterMs: null,
1196
+ packetLossPct: null,
1197
+ packetLossCumulativePct: null,
1198
+ packetsLost: null,
1199
+ packetsReceived: null,
1200
+ inboundBitrateBps: null,
1201
+ outboundBitrateBps: null,
1202
+ availableOutgoingBitrateBps: null,
1203
+ framesPerSecond: null,
1204
+ frameWidth: null,
1205
+ frameHeight: null,
1206
+ framesDropped: null,
1207
+ freezeCount: null,
1208
+ freezeDurationMs: null,
1209
+ concealedSamples: null,
1210
+ codec: null,
1211
+ transportProtocol: null,
1212
+ candidateType: null,
1213
+ qualityLimitationReason: null,
1214
+ deviceMemoryGb: me(),
1215
+ effectiveConnectionType: J()?.effectiveType ?? null
1216
+ }, ...i?.sample ?? {}, ...t, sampleType: e, sampledAt: (/* @__PURE__ */ new Date()).toISOString() }, a = ue(s);
1217
+ return s.networkScore = a.networkScore, s.estimatedMos = a.estimatedMos, this.latestQuality = s, s;
1218
+ }
1219
+ async reportQuality(e) {
1220
+ const t = this.options.credential.telemetryToken, i = this.options.telemetryEndpoint ?? this.options.credential.telemetryEndpoint;
1221
+ if (!t || !i) return;
1222
+ const n = await fetch(i, {
1223
+ method: "POST",
1224
+ headers: { authorization: `Bearer ${t}`, "content-type": "application/json" },
1225
+ body: JSON.stringify(e),
1226
+ keepalive: e.sampleType === "final"
1227
+ });
1228
+ if (!n.ok) throw new Error(`RTC telemetry failed with HTTP ${n.status}`);
1229
+ }
1230
+ getStats() {
1231
+ const e = this.client, t = e?.getLocalStream();
1232
+ return {
1233
+ state: this.state,
1234
+ participantCount: e ? e.getParticipants().length + 1 : 0,
1235
+ audioEnabled: !!t?.getAudioTracks().some((i) => i.enabled),
1236
+ videoEnabled: !!t?.getVideoTracks().some((i) => i.enabled),
1237
+ screenShareEnabled: !1,
1238
+ latestMediaScore: this.latestMediaScore,
1239
+ latestQuality: this.latestQuality,
1240
+ sampledAt: (/* @__PURE__ */ new Date()).toISOString()
1241
+ };
1242
+ }
1243
+ /** Escape hatch for advanced integrations. Prefer the stable SDK methods when possible. */
1244
+ getNativeClient() {
1245
+ return this.client;
1246
+ }
1247
+ destroy() {
1248
+ this.stopStats(), this.client && this.client.leave(), this.client = null, this.joinPromise = null, this.peerConnection = null, this.state = "disconnected";
1249
+ }
1250
+ handleNativeEvent(e, t) {
1251
+ 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);
1252
+ }
1253
+ startStats() {
1254
+ this.stopStats(), this.statsTimer = setInterval(() => {
1255
+ this.sampleAndReport("runtime");
1256
+ }, this.options.statsInterval);
1257
+ }
1258
+ async sampleAndReport(e) {
1259
+ if (!this.statsRunning) {
1260
+ this.statsRunning = !0;
1261
+ try {
1262
+ const t = await this.collectQualitySample(e);
1263
+ await this.adaptQuality(t), this.emit("stats", { ...this.getStats(), latestQuality: t });
1264
+ const i = Date.now(), n = e !== "runtime" || i - this.lastTelemetryAt >= this.options.telemetryInterval;
1265
+ this.options.autoReportStats && n && (await this.reportQuality(t), this.lastTelemetryAt = i);
1266
+ } catch (t) {
1267
+ this.emit("telemetryerror", g(t));
1268
+ } finally {
1269
+ this.statsRunning = !1;
1270
+ }
1271
+ }
1272
+ }
1273
+ stopStats() {
1274
+ this.statsTimer !== null && clearInterval(this.statsTimer), this.statsTimer = null;
1275
+ }
1276
+ async adaptQuality(e) {
1277
+ if (this.requestedQuality !== "auto" || this.options.adaptiveQuality === !1 || this.mode === "voice") return;
1278
+ if ((this.client?.getParticipants().length ?? 0) > 1) {
1279
+ this.poorQualitySamples = 0, this.goodQualitySamples = 0, this.effectiveQuality !== "high" && await this.applyEffectiveQuality("high", "network");
1280
+ return;
1281
+ }
1282
+ 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);
1283
+ this.poorQualitySamples = n ? this.poorQualitySamples + 1 : 0, this.goodQualitySamples = s ? this.goodQualitySamples + 1 : 0;
1284
+ const a = ["low", "medium", "high"], r = a.indexOf(this.effectiveQuality);
1285
+ i && r > 0 ? (this.poorQualitySamples = 0, await this.applyEffectiveQuality("low", "network")) : this.poorQualitySamples >= 2 && r > 0 ? (this.poorQualitySamples = 0, await this.applyEffectiveQuality(a[r - 1], "network")) : this.goodQualitySamples >= 6 && r >= 0 && r < a.length - 1 && (this.goodQualitySamples = 0, await this.applyEffectiveQuality(a[r + 1], "network"));
1286
+ }
1287
+ async applyEffectiveQuality(e, t) {
1288
+ if (this.effectiveQuality === e && this.client) return;
1289
+ const i = this.effectiveQuality;
1290
+ this.effectiveQuality = e, this.client && await this.client.setQuality(e), this.emit("qualitychange", { requestedQuality: this.requestedQuality, quality: e, previous: i, reason: t });
1291
+ }
1292
+ requireClient() {
1293
+ if (!this.client) throw new Error("Call initialize() before using the RTC client");
1294
+ return this.client;
1295
+ }
1296
+ emit(e, t) {
1297
+ const i = { type: e, detail: t, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
1298
+ this.dispatchEvent(new CustomEvent(e, { detail: i })), this.options.onEvent?.(i);
1299
+ }
1300
+ }
1301
+ async function ye(c) {
1302
+ return new re(c).initialize();
1303
+ }
1304
+ let P = null;
1305
+ function W() {
1306
+ if (P) return P;
1307
+ const c = globalThis, e = c.AudioContext ?? c.webkitAudioContext;
1308
+ return e ? (P = new e(), P) : null;
1309
+ }
1310
+ async function oe() {
1311
+ const c = W();
1312
+ return c ? (c.state === "suspended" && await c.resume().catch(() => {
1313
+ }), c.state === "running") : !1;
1314
+ }
1315
+ class ce {
1316
+ options;
1317
+ media = null;
1318
+ cadenceTimer = null;
1319
+ oscillators = /* @__PURE__ */ new Set();
1320
+ constructor(e) {
1321
+ this.options = e;
1322
+ }
1323
+ unlock() {
1324
+ return oe();
1325
+ }
1326
+ startIncoming() {
1327
+ this.start("incoming", this.options.ringtoneUrl);
1328
+ }
1329
+ startRingback() {
1330
+ this.start("ringback", this.options.ringbackUrl);
1331
+ }
1332
+ stop() {
1333
+ this.cadenceTimer && clearInterval(this.cadenceTimer), this.cadenceTimer = null, this.media?.pause(), this.media && (this.media.currentTime = 0), this.media = null;
1334
+ for (const e of this.oscillators)
1335
+ try {
1336
+ e.stop();
1337
+ } catch {
1338
+ }
1339
+ this.oscillators.clear();
1340
+ }
1341
+ start(e, t) {
1342
+ if (this.stop(), this.options.sounds === !1) return;
1343
+ if (t && typeof Audio < "u") {
1344
+ this.media = new Audio(t), this.media.loop = !0, this.media.play().catch(() => {
1345
+ });
1346
+ return;
1347
+ }
1348
+ const i = () => this.playBuiltInTone(e);
1349
+ i(), this.cadenceTimer = setInterval(i, e === "incoming" ? 3e3 : 3500);
1350
+ }
1351
+ playBuiltInTone(e) {
1352
+ const t = W();
1353
+ if (!t || t.state !== "running") return;
1354
+ const i = e === "incoming" ? 1.25 : 1, n = e === "incoming" ? [440, 480] : [425], s = t.createGain(), a = t.currentTime;
1355
+ 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);
1356
+ for (const r of n) {
1357
+ const o = t.createOscillator();
1358
+ o.type = "sine", o.frequency.value = r, o.connect(s), this.oscillators.add(o), o.onended = () => this.oscillators.delete(o), o.start(a), o.stop(a + i + 0.02);
1359
+ }
1360
+ }
1361
+ }
1362
+ class le extends EventTarget {
1363
+ options;
1364
+ socket = null;
1365
+ stopped = !1;
1366
+ reconnectAttempt = 0;
1367
+ reconnectTimer = null;
1368
+ heartbeatTimer = null;
1369
+ heartbeatDeadline = 0;
1370
+ currentCall = null;
1371
+ outgoingCallId = null;
1372
+ mount = null;
1373
+ terminalTimer = null;
1374
+ tokenRefreshTimer = null;
1375
+ tokenRequest = null;
1376
+ currentToken;
1377
+ currentTokenExpiresAt;
1378
+ callAudio;
1379
+ mediaClients = /* @__PURE__ */ new Map();
1380
+ activeCallIds = /* @__PURE__ */ new Set();
1381
+ seenInvitationIds = /* @__PURE__ */ new Set();
1382
+ startCallRequest = null;
1383
+ lastDisconnect = null;
1384
+ constructor(e) {
1385
+ if (super(), !e.token && !e.tokenProvider) throw new Error("A client token or tokenProvider is required");
1386
+ if (e.token && !e.token.startsWith("rtcc_")) throw new Error("A valid RTC client token is required");
1387
+ if (!e.signalingUrl && !e.apiBaseUrl && !globalThis.location?.origin)
1388
+ throw new Error("signalingUrl or apiBaseUrl is required outside a browser");
1389
+ this.options = e, this.currentToken = e.token ?? null, this.currentTokenExpiresAt = e.tokenExpiresAt, this.callAudio = new ce(e), this.mount = B(e.mount), this.scheduleTokenRefresh();
1390
+ }
1391
+ connect() {
1392
+ if (!this.options.signalingUrl) throw new Error("signalingUrl is required to connect signaling");
1393
+ return this.stopped = !1, this.scheduleTokenRefresh(), this.openSocket("initial"), this;
1394
+ }
1395
+ setMount(e) {
1396
+ this.clearUi(), this.mount = B(e), this.currentCall && this.renderIncoming(this.currentCall);
1397
+ }
1398
+ /** Binds media lifetime to a call. Terminal call states automatically leave and destroy it. */
1399
+ attachMediaClient(e, t) {
1400
+ if (!e.trim()) throw new Error("callId is required");
1401
+ return this.mediaClients.set(e, t), () => {
1402
+ this.mediaClients.get(e) === t && this.mediaClients.delete(e);
1403
+ };
1404
+ }
1405
+ /**
1406
+ * Rings everyone in `targets` as a single call and connects whoever answers
1407
+ * first; the rest stop ringing on their own. `target` still works and means a
1408
+ * call with one recipient.
1409
+ */
1410
+ async startCall(e) {
1411
+ if (this.startCallRequest) return this.startCallRequest;
1412
+ if (this.activeCallIds.size > 0) throw new Error("Another call is already active");
1413
+ const t = e.targets?.length ? e.targets : e.target ? [e.target] : [];
1414
+ if (t.length === 0) throw new Error("startCall requires at least one target");
1415
+ const i = { mode: e.mode, targets: t, metadata: e.metadata, idempotencyKey: e.idempotencyKey }, n = (async () => {
1416
+ await this.callAudio.unlock(), this.callAudio.startRingback();
1417
+ let s;
1418
+ try {
1419
+ s = await this.request("/v1/rtc/client/calls", {
1420
+ method: "POST",
1421
+ headers: e.idempotencyKey ? { "idempotency-key": e.idempotencyKey } : void 0,
1422
+ body: JSON.stringify(i)
1423
+ });
1424
+ } catch (a) {
1425
+ throw this.callAudio.stop(), a;
1426
+ }
1427
+ 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;
1428
+ })();
1429
+ this.startCallRequest = n;
1430
+ try {
1431
+ return await n;
1432
+ } finally {
1433
+ this.startCallRequest === n && (this.startCallRequest = null);
1434
+ }
1435
+ }
1436
+ async accept(e = this.currentCall?.id) {
1437
+ if (!e) throw new Error("No incoming call is available");
1438
+ const t = await this.callAction(e, "accept");
1439
+ return this.activeCallIds.add(e), this.callAudio.stop(), this.dismiss(e), this.emit("callaccepted", t), t;
1440
+ }
1441
+ async reject(e = this.currentCall?.id) {
1442
+ if (!e) throw new Error("No incoming call is available");
1443
+ const t = await this.callAction(e, "reject");
1444
+ return this.releaseMedia(e), this.showTerminal(e, "Call declined"), this.emit("callrejected", t), t;
1445
+ }
1446
+ async busy(e = this.currentCall?.id) {
1447
+ if (!e) throw new Error("No incoming call is available");
1448
+ const t = await this.callAction(e, "busy");
1449
+ return this.releaseMedia(e), this.showTerminal(e, "Line busy"), t;
1450
+ }
1451
+ async end(e) {
1452
+ this.callAudio.stop();
1453
+ const t = await this.callAction(e, "end");
1454
+ return this.releaseMedia(e), this.showTerminal(e, "Call ended"), t;
1455
+ }
1456
+ async cancel(e) {
1457
+ this.callAudio.stop();
1458
+ const t = await this.callAction(e, "cancel");
1459
+ return this.releaseMedia(e), this.showTerminal(e, "Call cancelled"), t;
1460
+ }
1461
+ /** Injects an incoming call received through the application's own push or signaling channel. */
1462
+ handleIncomingCall(e) {
1463
+ if (!e?.id || !e.caller?.id || !e.caller.name || e.mode !== "voice" && e.mode !== "video")
1464
+ throw new Error("Incoming call payload is invalid");
1465
+ if (e.expiresAt && Date.parse(e.expiresAt) <= Date.now()) {
1466
+ this.emit("callexpired", { id: e.id, expiresAt: e.expiresAt });
1467
+ return;
1468
+ }
1469
+ if (this.currentCall?.id === e.id) {
1470
+ this.currentCall = e, this.emit("callrestored", e);
1471
+ return;
1472
+ }
1473
+ if (this.seenInvitationIds.has(e.id)) {
1474
+ this.emit("callrestored", e);
1475
+ return;
1476
+ }
1477
+ this.seenInvitationIds.add(e.id), this.activeCallIds.add(e.id), this.currentCall = e, this.callAudio.startIncoming(), this.renderIncoming(e), this.emit("incomingcall", e);
1478
+ }
1479
+ /** Applies a call state received through the application's own push or signaling channel. */
1480
+ handleCallUpdate(e) {
1481
+ const t = e.id === this.currentCall?.id || e.id === this.outgoingCallId;
1482
+ 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, de(e.status)), ["rejected", "busy", "cancelled", "ended", "failed", "missed"].includes(e.status) && this.releaseMedia(e.id), this.emit("callupdated", e);
1483
+ }
1484
+ disconnect() {
1485
+ 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();
1486
+ for (const e of this.mediaClients.keys()) this.releaseMedia(e);
1487
+ this.activeCallIds.clear(), this.seenInvitationIds.clear();
1488
+ }
1489
+ destroy() {
1490
+ this.disconnect(), this.mount = null;
1491
+ }
1492
+ async openSocket(e) {
1493
+ if (!this.options.signalingUrl || this.stopped || this.socket?.readyState === WebSocket.OPEN || this.socket?.readyState === WebSocket.CONNECTING) return;
1494
+ let t;
1495
+ try {
1496
+ t = await this.resolveToken(e, e === "reconnect");
1497
+ } catch (s) {
1498
+ this.emit("tokenerror", g(s)), !this.stopped && this.options.reconnect !== !1 && this.scheduleReconnect();
1499
+ return;
1500
+ }
1501
+ if (this.stopped || this.socket) return;
1502
+ const i = new URL(this.options.signalingUrl, globalThis.location?.href);
1503
+ i.protocol = i.protocol === "https:" ? "wss:" : "ws:";
1504
+ const n = new WebSocket(i, ["rtc-client", t]);
1505
+ this.socket = n, n.addEventListener("open", () => {
1506
+ const s = this.reconnectAttempt > 0, a = this.lastDisconnect;
1507
+ 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(() => {
1508
+ if (n.readyState === WebSocket.OPEN) {
1509
+ if (Date.now() > this.heartbeatDeadline) {
1510
+ n.close(4e3, "Signaling heartbeat timed out");
1511
+ return;
1512
+ }
1513
+ n.send("ping");
1514
+ }
1515
+ }, 2e4);
1516
+ }), n.addEventListener("message", (s) => {
1517
+ this.heartbeatDeadline = Date.now() + 45e3, this.handleSignal(String(s.data));
1518
+ }), n.addEventListener("close", (s) => {
1519
+ if (this.socket !== n) return;
1520
+ this.socket = null, this.heartbeatTimer && clearInterval(this.heartbeatTimer), this.heartbeatTimer = null;
1521
+ const a = !this.stopped && this.options.reconnect !== !1, r = {
1522
+ code: s.code,
1523
+ reason: s.reason,
1524
+ wasClean: s.wasClean,
1525
+ willReconnect: a,
1526
+ attempt: this.reconnectAttempt + 1
1527
+ };
1528
+ a && (this.lastDisconnect = { code: s.code, reason: s.reason, wasClean: s.wasClean, at: Date.now() }), this.emit("signalingdisconnected", r), this.reportSignalingDiagnostic({ event: a ? "reconnecting" : "disconnected", ...r }), a && this.scheduleReconnect();
1529
+ }), n.addEventListener("error", () => {
1530
+ const s = { message: "RTC signaling connection failed", reconnecting: !this.stopped && this.options.reconnect !== !1, attempt: this.reconnectAttempt + 1 };
1531
+ this.emit("signalingerror", s), this.reportSignalingDiagnostic({ event: "error", attempt: s.attempt });
1532
+ });
1533
+ }
1534
+ handleSignal(e) {
1535
+ let t;
1536
+ try {
1537
+ t = JSON.parse(e);
1538
+ } catch {
1539
+ return;
1540
+ }
1541
+ const i = String(t.type ?? "");
1542
+ if (i === "call.incoming" && t.call && typeof t.call == "object") {
1543
+ this.handleIncomingCall(t.call);
1544
+ return;
1545
+ }
1546
+ if (i === "call.updated") {
1547
+ const n = t.call;
1548
+ n?.id && n.status && this.handleCallUpdate({ id: n.id, status: n.status });
1549
+ return;
1550
+ }
1551
+ i === "presence" ? this.emit("presence", { users: t.users ?? [] }) : i !== "pong" && this.emit(i || "signal", t);
1552
+ }
1553
+ async callAction(e, t) {
1554
+ return this.request(`/v1/rtc/client/calls/${encodeURIComponent(e)}/${t}`, {
1555
+ method: "POST",
1556
+ body: "{}"
1557
+ });
1558
+ }
1559
+ async request(e, t, i = !1) {
1560
+ const n = this.options.apiBaseUrl ?? this.options.signalingUrl ?? globalThis.location?.origin;
1561
+ if (!n) throw new Error("apiBaseUrl is required for call actions");
1562
+ const s = new URL(n, globalThis.location?.href), a = await this.resolveToken("initial"), r = new Headers(t.headers);
1563
+ r.set("authorization", `Bearer ${a}`), r.set("content-type", "application/json");
1564
+ const o = await fetch(new URL(e, s.origin), { ...t, headers: r }), l = await o.json().catch(() => ({}));
1565
+ if (o.status === 401 && this.options.tokenProvider && !i)
1566
+ return await this.resolveToken("unauthorized", !0), this.request(e, t, !0);
1567
+ if (!o.ok) {
1568
+ const d = l.error;
1569
+ throw new Error(typeof d == "string" ? d : d?.message || `RTC API failed with HTTP ${o.status}`);
1570
+ }
1571
+ return l;
1572
+ }
1573
+ renderIncoming(e) {
1574
+ if (!this.mount) return;
1575
+ this.clearUi();
1576
+ const t = document.createElement("section");
1577
+ 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, {
1578
+ display: "grid",
1579
+ gap: "14px",
1580
+ padding: "20px",
1581
+ border: "1px solid #cbd8dc",
1582
+ borderRadius: "8px",
1583
+ background: "#fff",
1584
+ color: "#102027",
1585
+ boxShadow: "0 14px 38px #07101429",
1586
+ fontFamily: "system-ui, sans-serif"
1587
+ });
1588
+ const i = document.createElement("small");
1589
+ i.textContent = e.mode === "video" ? "Incoming video call" : "Incoming voice call", i.style.color = "#5d7079";
1590
+ const n = document.createElement("strong");
1591
+ n.textContent = e.caller.name, n.style.fontSize = "20px";
1592
+ const s = document.createElement("div");
1593
+ Object.assign(s.style, { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px" });
1594
+ const a = U("Decline", "#fff", "#a32920", "#d9a09b"), r = U("Answer", "#fff", "#08745f", "#08745f");
1595
+ a.addEventListener("click", () => {
1596
+ this.reject(e.id).catch((o) => this.emit("error", g(o)));
1597
+ }), r.addEventListener("click", () => {
1598
+ this.accept(e.id).catch((o) => this.emit("error", g(o)));
1599
+ }), s.appendChild(a), s.appendChild(r), t.appendChild(i), t.appendChild(n), t.appendChild(s), this.mount.replaceChildren(t);
1600
+ }
1601
+ dismiss(e) {
1602
+ e && this.currentCall?.id !== e && this.outgoingCallId !== e || (this.currentCall = null, (!e || this.outgoingCallId === e) && (this.outgoingCallId = null), this.callAudio.stop(), this.clearUi());
1603
+ }
1604
+ showTerminal(e, t) {
1605
+ if (this.callAudio.stop(), this.currentCall?.id === e && (this.currentCall = null), this.outgoingCallId === e && (this.outgoingCallId = null), !this.mount) return;
1606
+ this.clearUi();
1607
+ const i = document.createElement("section");
1608
+ i.dataset.rtcCallTerminal = e, i.setAttribute("role", "status"), Object.assign(i.style, {
1609
+ display: "grid",
1610
+ placeItems: "center",
1611
+ gap: "10px",
1612
+ minHeight: "150px",
1613
+ padding: "20px",
1614
+ border: "1px solid #cbd8dc",
1615
+ borderRadius: "8px",
1616
+ background: "#fff",
1617
+ color: "#102027",
1618
+ boxShadow: "0 14px 38px #07101429",
1619
+ fontFamily: "system-ui, sans-serif"
1620
+ });
1621
+ const n = document.createElement("strong");
1622
+ n.textContent = t, n.style.fontSize = "18px", i.appendChild(n), this.mount.replaceChildren(i);
1623
+ const s = Math.max(0, this.options.terminalStateDurationMs ?? 1600);
1624
+ this.terminalTimer = setTimeout(() => this.clearUi(), s);
1625
+ }
1626
+ clearUi() {
1627
+ this.terminalTimer && clearTimeout(this.terminalTimer), this.terminalTimer = null, this.mount && this.mount.replaceChildren();
1628
+ }
1629
+ scheduleReconnect() {
1630
+ this.reconnectTimer && clearTimeout(this.reconnectTimer);
1631
+ const e = Math.min(3e4, 1e3 * 2 ** Math.min(this.reconnectAttempt, 5)) + Math.round(Math.random() * 300);
1632
+ this.reconnectAttempt += 1, this.reconnectTimer = setTimeout(() => {
1633
+ this.openSocket("reconnect");
1634
+ }, e);
1635
+ }
1636
+ async resolveToken(e, t = !1) {
1637
+ if (!t && this.currentToken && !this.tokenNeedsRefresh()) return this.currentToken;
1638
+ if (!this.options.tokenProvider) {
1639
+ if (!this.currentToken) throw new Error("A valid RTC client token is required");
1640
+ return this.currentToken;
1641
+ }
1642
+ return this.tokenRequest ? this.tokenRequest : (this.tokenRequest = (async () => {
1643
+ const i = await this.options.tokenProvider?.({ reason: e, previousExpiresAt: this.currentTokenExpiresAt }), n = typeof i == "string" ? { token: i } : i;
1644
+ if (!n?.token?.startsWith("rtcc_")) throw new Error("tokenProvider returned an invalid RTC client token");
1645
+ return this.currentToken = n.token, this.currentTokenExpiresAt = n.expiresAt, this.scheduleTokenRefresh(), this.emit("tokenrefreshed", { reason: e, expiresAt: n.expiresAt }), n.token;
1646
+ })().finally(() => {
1647
+ this.tokenRequest = null;
1648
+ }), this.tokenRequest);
1649
+ }
1650
+ tokenNeedsRefresh() {
1651
+ if (!this.currentTokenExpiresAt) return !1;
1652
+ const e = Date.parse(this.currentTokenExpiresAt) - Date.now();
1653
+ return Number.isFinite(e) ? e <= this.refreshSkew(e) : !0;
1654
+ }
1655
+ refreshSkew(e) {
1656
+ const t = Math.max(5e3, this.options.tokenRefreshSkewMs ?? 3e5);
1657
+ return Math.min(t, Math.max(5e3, e * 0.2));
1658
+ }
1659
+ scheduleTokenRefresh() {
1660
+ if (this.tokenRefreshTimer && clearTimeout(this.tokenRefreshTimer), this.tokenRefreshTimer = null, this.stopped || !this.options.tokenProvider || !this.currentTokenExpiresAt) return;
1661
+ const e = Date.parse(this.currentTokenExpiresAt) - Date.now();
1662
+ if (!Number.isFinite(e)) return;
1663
+ const t = Math.max(0, e - this.refreshSkew(e));
1664
+ this.tokenRefreshTimer = setTimeout(() => {
1665
+ this.tokenRefreshTimer = null, this.resolveToken("expiring", !0).catch((i) => {
1666
+ this.emit("tokenerror", g(i)), this.stopped || (this.tokenRefreshTimer = setTimeout(() => this.scheduleTokenRefresh(), 3e4));
1667
+ });
1668
+ }, Math.min(t, 2147e6));
1669
+ }
1670
+ releaseMedia(e) {
1671
+ this.activeCallIds.delete(e), this.seenInvitationIds.delete(e);
1672
+ const t = this.mediaClients.get(e);
1673
+ t && (this.mediaClients.delete(e), t.leave().catch(() => {
1674
+ }).finally(() => t.destroy()));
1675
+ }
1676
+ reportSignalingDiagnostic(e) {
1677
+ this.options.reportSignalingDiagnostics === !1 || this.stopped || this.request("/v1/rtc/client/signaling-events", {
1678
+ method: "POST",
1679
+ body: JSON.stringify({ ...e, occurredAt: (/* @__PURE__ */ new Date()).toISOString() })
1680
+ }).catch((t) => this.emit("diagnosticserror", g(t)));
1681
+ }
1682
+ emit(e, t) {
1683
+ const i = { type: e, detail: t, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
1684
+ this.dispatchEvent(new CustomEvent(e, { detail: i })), this.options.onEvent?.(i);
1685
+ }
1686
+ }
1687
+ function Se(c) {
1688
+ const e = new le(c);
1689
+ return c.autoConnect !== !1 && c.signalingUrl && e.connect(), e;
1690
+ }
1691
+ function B(c) {
1692
+ return c ? typeof c == "string" ? document.querySelector(c) : c : null;
1693
+ }
1694
+ function U(c, e, t, i) {
1695
+ const n = document.createElement("button");
1696
+ return n.type = "button", n.textContent = c, Object.assign(n.style, {
1697
+ minHeight: "44px",
1698
+ border: `1px solid ${i}`,
1699
+ borderRadius: "6px",
1700
+ background: t,
1701
+ color: e,
1702
+ cursor: "pointer",
1703
+ font: "inherit",
1704
+ fontWeight: "700"
1705
+ }), n;
1706
+ }
1707
+ function de(c) {
1708
+ return {
1709
+ rejected: "Call declined",
1710
+ busy: "Line busy",
1711
+ cancelled: "Call cancelled",
1712
+ ended: "Call ended",
1713
+ failed: "Call failed",
1714
+ missed: "No answer"
1715
+ }[c] ?? "Call ended";
1716
+ }
1717
+ function F(c) {
1718
+ return {
1719
+ id: c.participantId,
1720
+ name: c.displayName,
1721
+ role: "participant"
1722
+ };
1723
+ }
1724
+ async function he(c, e) {
1725
+ const t = await c.getStats(), i = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
1726
+ let s = 0, a = 0, r = 0, o = 0, l = 0, d = 0, u = 0, m = 0, p = 0, v = 0, T = 0, E = 0, w = 0, y = null, b = null, C = null, S = null, k = null, I = null;
1727
+ t.forEach((h) => {
1728
+ n.set(h.id, h), h.type === "codec" && i.set(h.id, String(h.mimeType ?? ""));
1729
+ }), t.forEach((h) => {
1730
+ if (h.type === "inbound-rtp" && !h.isRemote && (s += Number(h.bytesReceived ?? 0), r += Math.max(0, Number(h.packetsLost ?? 0)), o += Math.max(0, Number(h.packetsReceived ?? 0)), Number.isFinite(h.jitter) && (l += Number(h.jitter), d += 1), u = Math.max(u, Number(h.framesPerSecond ?? 0)), m = Math.max(m, Number(h.frameWidth ?? 0)), p = Math.max(p, Number(h.frameHeight ?? 0)), v += Number(h.framesDropped ?? 0), T += Number(h.freezeCount ?? 0), E += Number(h.totalFreezesDuration ?? 0), w += Number(h.concealedSamples ?? 0), k ||= i.get(String(h.codecId ?? "")) ?? null), h.type === "outbound-rtp" && !h.isRemote && (a += Number(h.bytesSent ?? 0), I ||= h.qualityLimitationReason ? String(h.qualityLimitationReason) : null, k ||= i.get(String(h.codecId ?? "")) ?? null), h.type === "candidate-pair" && (h.selected || h.nominated) && h.state === "succeeded") {
1731
+ y = Number.isFinite(h.currentRoundTripTime) ? Number(h.currentRoundTripTime) : y, b = Number.isFinite(h.availableOutgoingBitrate) ? Number(h.availableOutgoingBitrate) : b;
1732
+ const R = n.get(h.localCandidateId);
1733
+ C = R?.candidateType ? String(R.candidateType) : C, S = R?.protocol ? String(R.protocol) : S;
1734
+ }
1735
+ });
1736
+ const D = Date.now(), q = e ? Math.max(1e-3, (D - e.timestamp) / 1e3) : 0, G = e ? Math.max(0, Math.round((s - e.inboundBytes) * 8 / q)) : null, K = e ? Math.max(0, Math.round((a - e.outboundBytes) * 8 / q)) : null, N = r + o, Q = e ? Math.max(0, r - e.packetsLost) : r, Y = e ? Math.max(0, o - e.packetsReceived) : o, V = Q + Y;
1737
+ return {
1738
+ counters: { timestamp: D, inboundBytes: s, outboundBytes: a, packetsLost: r, packetsReceived: o },
1739
+ sample: {
1740
+ rttMs: y === null ? null : y * 1e3,
1741
+ jitterMs: d ? l / d * 1e3 : null,
1742
+ packetLossPct: V ? Q / V * 100 : null,
1743
+ packetLossCumulativePct: N ? r / N * 100 : null,
1744
+ packetsLost: r,
1745
+ packetsReceived: o,
1746
+ inboundBitrateBps: G,
1747
+ outboundBitrateBps: K,
1748
+ availableOutgoingBitrateBps: b,
1749
+ framesPerSecond: u || null,
1750
+ frameWidth: m || null,
1751
+ frameHeight: p || null,
1752
+ framesDropped: v,
1753
+ freezeCount: T,
1754
+ freezeDurationMs: Math.round(E * 1e3),
1755
+ concealedSamples: w,
1756
+ codec: k,
1757
+ transportProtocol: S,
1758
+ candidateType: C,
1759
+ qualityLimitationReason: I
1760
+ }
1761
+ };
1762
+ }
1763
+ function ue(c) {
1764
+ let e = c.mediaScore == null ? 5 : Math.round($(Number(c.mediaScore), 0, 5));
1765
+ const t = x(c.packetLossPct) ?? 0, i = x(c.rttMs) ?? 0, n = x(c.jitterMs) ?? 0, s = String(c.connectionState ?? "unknown").toLowerCase();
1766
+ ["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));
1767
+ const a = Number($(4.5 - t * 0.09 - i * 11e-4 - n * 6e-3, 1, 4.5).toFixed(2));
1768
+ return { networkScore: e, estimatedMos: a };
1769
+ }
1770
+ function J() {
1771
+ return navigator.connection ?? null;
1772
+ }
1773
+ function me() {
1774
+ const c = Number(navigator.deviceMemory);
1775
+ return Number.isFinite(c) ? c : null;
1776
+ }
1777
+ function x(c) {
1778
+ const e = Number(c);
1779
+ return Number.isFinite(e) ? e : null;
1780
+ }
1781
+ function $(c, e, t) {
1782
+ return Math.min(t, Math.max(e, c));
1783
+ }
1784
+ function g(c) {
1785
+ return c instanceof Error ? { name: c.name, message: c.message } : { name: "ServiceError", message: String(c ?? "Service request failed") };
1786
+ }
1787
+ function z(c, e) {
1788
+ return c instanceof Error ? c : new Error(e);
1789
+ }
1790
+ const pe = "1.3.14";
1791
+ export {
1792
+ re as RtcClient,
1793
+ le as RtcIncomingClient,
1794
+ fe as SPEAKER_NOISE_FLOOR,
1795
+ ge as SPEAKER_TAKEOVER_RATIO,
1796
+ ve as createPictureInPictureController,
1797
+ ye as createRtcClient,
1798
+ Se as createRtcIncomingClient,
1799
+ H as pickActiveSpeaker,
1800
+ oe as unlockRtcCallAudio,
1801
+ pe as version
1802
+ };