@encarnage7/local-meet 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,2558 @@
1
+ /**
2
+ * Local Meet SDK v1.0.0 (CommonJS)
3
+ * Zero-trust WebRTC P2P Mesh & Cloudflare Calls SFU client library
4
+ * License: MIT
5
+ */
6
+
7
+ /**
8
+ * Eventos emitidos por LocalMeetRoom
9
+ */
10
+ const RoomEvents = Object.freeze({
11
+ ParticipantConnected: "participantConnected",
12
+ ParticipantDisconnected: "participantDisconnected",
13
+ ParticipantMetadataChanged: "participantMetadataChanged",
14
+ ParticipantPermissionsChanged: "participantPermissionsChanged",
15
+ DataReceived: "dataReceived",
16
+ TrackSubscribed: "trackSubscribed",
17
+ TrackUnsubscribed: "trackUnsubscribed",
18
+ TrackUnpublished: "trackUnpublished",
19
+ LocalTrackPublished: "localTrackPublished",
20
+ LocalTrackUnpublished: "localTrackUnpublished",
21
+ TrackMuted: "trackMuted",
22
+ TrackUnmuted: "trackUnmuted",
23
+ ActiveSpeakersChanged: "activeSpeakersChanged",
24
+ Disconnected: "disconnected",
25
+ Error: "error",
26
+ });
27
+
28
+ const TrackSources = Object.freeze({
29
+ Camera: "camera",
30
+ Microphone: "microphone",
31
+ ScreenShare: "screen_share",
32
+ ScreenShareAudio: "screen_share_audio",
33
+ });
34
+
35
+ const TrackKinds = Object.freeze({
36
+ Audio: "audio",
37
+ Video: "video",
38
+ });
39
+
40
+ const EngineModes = Object.freeze({
41
+ CloudflareCalls: "cf-calls",
42
+ P2PMesh: "p2p",
43
+ LiveKit: "livekit",
44
+ });
45
+
46
+
47
+ /**
48
+ * Cifrado de paquetes de datos de sala con AES-GCM-256 usando Web Crypto API.
49
+ */
50
+
51
+ const PBKDF2_ITERATIONS = 100000;
52
+ const cachedAesKeys = new Map();
53
+
54
+ function bytesToBase64(bytes) {
55
+ let binary = "";
56
+ for (let i = 0; i < bytes.byteLength; i++) {
57
+ binary += String.fromCharCode(bytes[i]);
58
+ }
59
+ return btoa(binary);
60
+ }
61
+
62
+ function base64ToBytes(base64) {
63
+ const binary = atob(base64);
64
+ const out = new Uint8Array(binary.length);
65
+ for (let i = 0; i < binary.length; i++) {
66
+ out[i] = binary.charCodeAt(i);
67
+ }
68
+ return out;
69
+ }
70
+
71
+ async function deriveRoomAesKey(roomHash, extraSecret = "") {
72
+ const cleanHash = (roomHash || "").toLowerCase().trim();
73
+ const cacheKey = `${cleanHash}::${extraSecret}`;
74
+ if (cachedAesKeys.has(cacheKey)) {
75
+ return cachedAesKeys.get(cacheKey);
76
+ }
77
+
78
+ const salt = new TextEncoder().encode(`local-meet-salt-v1:${cleanHash}`);
79
+ const keyMaterial = await crypto.subtle.importKey(
80
+ "raw",
81
+ new TextEncoder().encode(`${cleanHash}:${extraSecret}`),
82
+ "PBKDF2",
83
+ false,
84
+ ["deriveKey"]
85
+ );
86
+
87
+ const aesKey = await crypto.subtle.deriveKey(
88
+ {
89
+ name: "PBKDF2",
90
+ salt,
91
+ iterations: PBKDF2_ITERATIONS,
92
+ hash: "SHA-256",
93
+ },
94
+ keyMaterial,
95
+ { name: "AES-GCM", length: 256 },
96
+ false,
97
+ ["encrypt", "decrypt"]
98
+ );
99
+
100
+ cachedAesKeys.set(cacheKey, aesKey);
101
+ return aesKey;
102
+ }
103
+
104
+ async function encryptDataPacket(roomHash, plaintext, extraSecret = "") {
105
+ const aesKey = await deriveRoomAesKey(roomHash, extraSecret);
106
+ const iv = new Uint8Array(12);
107
+ crypto.getRandomValues(iv);
108
+ const encoded = new TextEncoder().encode(plaintext);
109
+ const ciphertext = await crypto.subtle.encrypt(
110
+ { name: "AES-GCM", iv },
111
+ aesKey,
112
+ encoded
113
+ );
114
+ return {
115
+ e2ee: "aes-gcm-256",
116
+ iv: bytesToBase64(iv),
117
+ ct: bytesToBase64(new Uint8Array(ciphertext)),
118
+ };
119
+ }
120
+
121
+ async function decryptDataPacket(roomHash, packet, extraSecret = "") {
122
+ if (!packet || packet.e2ee !== "aes-gcm-256" || !packet.iv || !packet.ct) {
123
+ return null;
124
+ }
125
+ try {
126
+ const aesKey = await deriveRoomAesKey(roomHash, extraSecret);
127
+ const iv = base64ToBytes(packet.iv);
128
+ const ct = base64ToBytes(packet.ct);
129
+ const decryptedBuffer = await crypto.subtle.decrypt(
130
+ { name: "AES-GCM", iv },
131
+ aesKey,
132
+ ct
133
+ );
134
+ return new TextDecoder().decode(decryptedBuffer);
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+
141
+
142
+
143
+ const DEFAULT_ICE_SERVERS = [
144
+ { urls: "stun:stun.cloudflare.com:3478" },
145
+ { urls: "stun:stun.l.google.com:19302" },
146
+ { urls: "stun:stun1.l.google.com:19302" },
147
+ {
148
+ urls: [
149
+ "turn:openrelay.metered.ca:80",
150
+ "turn:openrelay.metered.ca:443",
151
+ "turn:openrelay.metered.ca:443?transport=tcp",
152
+ "turns:openrelay.metered.ca:443?transport=tcp",
153
+ ],
154
+ username: "openrelayproject",
155
+ credential: "openrelayproject",
156
+ },
157
+ ];
158
+
159
+ class HybridTrack {
160
+ constructor(mediaStreamTrack, kind, source) {
161
+ this.mediaStreamTrack = mediaStreamTrack;
162
+ this.kind = kind;
163
+ this.source = source;
164
+ this.mediaStream = mediaStreamTrack ? new MediaStream([mediaStreamTrack]) : null;
165
+ this.primaryElement = null;
166
+ this.attachedElements = new Set();
167
+ }
168
+
169
+ attach(existingElement = null) {
170
+ if (this.kind === TrackKinds.Video && this.primaryElement) {
171
+ const el = this.primaryElement;
172
+ el.autoplay = true;
173
+ el.playsInline = true;
174
+ el.muted = true;
175
+ if (this.mediaStream && el.srcObject !== this.mediaStream) {
176
+ el.srcObject = this.mediaStream;
177
+ }
178
+ this.attachedElements.add(el);
179
+ return el;
180
+ }
181
+
182
+ const el =
183
+ existingElement ||
184
+ document.createElement(this.kind === TrackKinds.Audio ? "audio" : "video");
185
+ el.autoplay = true;
186
+ el.playsInline = true;
187
+ if (this.kind === TrackKinds.Video) {
188
+ el.muted = true;
189
+ el.setAttribute("muted", "");
190
+ el.setAttribute("playsinline", "");
191
+ el.setAttribute("autoplay", "");
192
+ }
193
+ if (this.mediaStream) {
194
+ el.srcObject = this.mediaStream;
195
+ }
196
+ const ensurePlay = () => {
197
+ el.play?.().catch(() => {});
198
+ };
199
+ el.addEventListener("loadedmetadata", ensurePlay);
200
+ el.addEventListener("loadeddata", ensurePlay);
201
+ if (this.mediaStreamTrack) {
202
+ this.mediaStreamTrack.addEventListener("unmute", ensurePlay);
203
+ }
204
+ if (this.kind === TrackKinds.Video) {
205
+ this.primaryElement = el;
206
+ }
207
+ this.attachedElements.add(el);
208
+ return el;
209
+ }
210
+
211
+ detach() {
212
+ const list = Array.from(this.attachedElements);
213
+ for (const el of list) {
214
+ try {
215
+ el.srcObject = null;
216
+ } catch {
217
+ // Ignorar
218
+ }
219
+ }
220
+ this.primaryElement = null;
221
+ this.attachedElements.clear();
222
+ return list;
223
+ }
224
+
225
+ stop() {
226
+ try {
227
+ this.mediaStreamTrack?.stop();
228
+ } catch {
229
+ // Ignorar
230
+ }
231
+ }
232
+ }
233
+
234
+ class HybridPublication {
235
+ constructor({ trackSid, source, track = null, isMuted = false }) {
236
+ this.trackSid = trackSid;
237
+ this.source = source;
238
+ this.track = track;
239
+ this.isMuted = isMuted;
240
+ }
241
+ }
242
+
243
+ class HybridParticipant {
244
+ constructor({ identity, name, metadata = "{}", permissions = {}, isLocal = false, room = null }) {
245
+ this.identity = identity;
246
+ this.name = name;
247
+ this.metadata = metadata;
248
+ this.permissions = {
249
+ canPublish: Boolean(permissions.canPublish),
250
+ canSubscribe: Boolean(permissions.canSubscribe),
251
+ canPublishData: Boolean(permissions.canPublishData),
252
+ };
253
+ this.isLocal = isLocal;
254
+ this.room = room;
255
+ this.trackPublications = new Map();
256
+ this.videoTrackPublications = new Map();
257
+ this.peerId = "";
258
+ this.cfSessionId = "";
259
+ this.cfPublishedTracks = [];
260
+ }
261
+
262
+ getTrackPublication(source) {
263
+ return this.trackPublications.get(source) || null;
264
+ }
265
+
266
+ setPublication(source, publication) {
267
+ if (!publication) {
268
+ this.trackPublications.delete(source);
269
+ this.videoTrackPublications.delete(source);
270
+ return;
271
+ }
272
+ this.trackPublications.set(source, publication);
273
+ if (source === TrackSources.Camera || source === TrackSources.ScreenShare) {
274
+ this.videoTrackPublications.set(source, publication);
275
+ }
276
+ }
277
+
278
+ async enableCameraAndMicrophone() {
279
+ if (!this.isLocal || !this.room) return;
280
+ await this.room.enableCameraAndMicrophone();
281
+ }
282
+
283
+ async setMicrophoneEnabled(enabled) {
284
+ if (!this.isLocal || !this.room) return;
285
+ await this.room.setMicrophoneEnabled(Boolean(enabled));
286
+ }
287
+
288
+ async setCameraEnabled(enabled) {
289
+ if (!this.isLocal || !this.room) return;
290
+ await this.room.setCameraEnabled(Boolean(enabled));
291
+ }
292
+
293
+ async setScreenShareEnabled(enabled) {
294
+ if (!this.isLocal || !this.room) return null;
295
+ return this.room.setScreenShareEnabled(Boolean(enabled));
296
+ }
297
+
298
+ async publishData(payloadUint8, options = {}) {
299
+ if (!this.isLocal || !this.room) return;
300
+ await this.room.sendData(payloadUint8, options);
301
+ }
302
+ }
303
+
304
+ class LocalMeetRoom {
305
+ constructor(options = {}) {
306
+ const {
307
+ serverUrl = typeof window !== "undefined" ? window.location.origin : "",
308
+ roomHash = "",
309
+ title = "",
310
+ identity = "",
311
+ displayName = "Participante",
312
+ isHost = false,
313
+ hostSecret = "",
314
+ engine = EngineModes.CloudflareCalls,
315
+ cfCallsReady = false,
316
+ startInWaitingRoom = false,
317
+ iceServers = [],
318
+ debug = false,
319
+ } = options;
320
+
321
+ this.serverUrl = (serverUrl || "").replace(/\/+$/, "");
322
+ this.roomHash = (roomHash || "").toLowerCase().trim();
323
+ this.roomTitle = title || `Sala ${this.roomHash}`;
324
+ this.isHost = Boolean(isHost);
325
+ this.hostSecret = hostSecret || "";
326
+ this.engine = engine;
327
+ this.cfCallsReady = Boolean(cfCallsReady && engine === EngineModes.CloudflareCalls);
328
+ this.debug = Boolean(debug);
329
+
330
+ this.iceServers =
331
+ Array.isArray(iceServers) && iceServers.length > 0 ? iceServers : DEFAULT_ICE_SERVERS;
332
+
333
+ this.listeners = new Map();
334
+ this.remoteParticipants = new Map();
335
+
336
+ const localId = identity || `usr_${Math.random().toString(36).slice(2, 10)}`;
337
+ this.localParticipant = new HybridParticipant({
338
+ identity: localId,
339
+ name: displayName,
340
+ metadata: JSON.stringify({
341
+ role: this.isHost ? "host" : "guest",
342
+ status: startInWaitingRoom ? "waiting" : "approved",
343
+ room_title: this.roomTitle,
344
+ }),
345
+ permissions: {
346
+ canPublish: !startInWaitingRoom,
347
+ canSubscribe: !startInWaitingRoom,
348
+ canPublishData: !startInWaitingRoom,
349
+ },
350
+ isLocal: true,
351
+ room: this,
352
+ });
353
+
354
+ this.peer = null;
355
+ this.hostConn = null;
356
+ this.knownHostPeerId = this.getHostPeerId();
357
+ this.guestConns = new Map();
358
+ this.activeMediaCalls = new Map();
359
+ this.directScreenPcs = new Map();
360
+ this.incomingScreenPcs = new Map();
361
+ this.pendingScreenIce = new Map();
362
+ this.rtcSignalQueues = new Map();
363
+
364
+ this.joinedAt = Date.now();
365
+ this.outgoingHttpSignals = [];
366
+ this.processedSignalIds = new Set();
367
+ this.syncTimer = null;
368
+ this.peerKeepAliveTimer = null;
369
+ this.syncInFlight = false;
370
+
371
+ // Estado Cloudflare Calls SFU
372
+ this.cfSessionId = "";
373
+ this.cfPc = null;
374
+ this.cfInitPromise = null;
375
+ this.cfTransceivers = new Map();
376
+ this.cfPulledRemoteKeys = new Set();
377
+ this.cfPullRetryCounts = new Map();
378
+ this.cfMidToRemoteMeta = new Map();
379
+ this.cfNegotiationChain = Promise.resolve();
380
+ this.currentScreenTrackName = "screen";
381
+ this.isDisconnected = false;
382
+ }
383
+
384
+ log(...args) {
385
+ if (this.debug) {
386
+ console.log("[LocalMeet]", ...args);
387
+ }
388
+ }
389
+
390
+ async apiPost(endpoint, payload) {
391
+ const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
392
+ const url = `${this.serverUrl}${cleanEndpoint}`;
393
+
394
+ const res = await fetch(url, {
395
+ method: "POST",
396
+ headers: { "Content-Type": "application/json" },
397
+ body: JSON.stringify(payload),
398
+ });
399
+
400
+ const rawText = await res.text();
401
+ let data = null;
402
+ if (rawText) {
403
+ try {
404
+ data = JSON.parse(rawText);
405
+ } catch {
406
+ data = null;
407
+ }
408
+ }
409
+
410
+ if (!res.ok) {
411
+ throw new Error(
412
+ data?.detail || `Error en servidor (${res.status}): ${rawText.slice(0, 120)}`
413
+ );
414
+ }
415
+ return data;
416
+ }
417
+
418
+ on(event, callback) {
419
+ if (!this.listeners.has(event)) {
420
+ this.listeners.set(event, new Set());
421
+ }
422
+ this.listeners.get(event).add(callback);
423
+ return this;
424
+ }
425
+
426
+ off(event, callback) {
427
+ const set = this.listeners.get(event);
428
+ if (set) {
429
+ set.delete(callback);
430
+ }
431
+ return this;
432
+ }
433
+
434
+ emit(event, ...args) {
435
+ const set = this.listeners.get(event);
436
+ if (!set) return;
437
+ for (const cb of set) {
438
+ try {
439
+ cb(...args);
440
+ } catch (err) {
441
+ if (this.debug) console.error(`[LocalMeet] Error en listener de '${event}':`, err);
442
+ }
443
+ }
444
+ }
445
+
446
+ getHostPeerId() {
447
+ const cleanHash = this.roomHash.replace(/[^a-z0-9]/g, "");
448
+ return `lm-host-${cleanHash}`;
449
+ }
450
+
451
+ getMyPeerId() {
452
+ if (this.isHost) {
453
+ return this.getHostPeerId();
454
+ }
455
+ const cleanId = this.localParticipant.identity.replace(/[^a-z0-9]/g, "");
456
+ return `lm-${cleanId}`;
457
+ }
458
+
459
+ async connect() {
460
+ const myPeerId = this.getMyPeerId();
461
+ this.localParticipant.peerId = myPeerId;
462
+
463
+ if (typeof window !== "undefined" && typeof window.Peer === "function") {
464
+ await this.initPeerWithRetry(myPeerId, 3).catch(() => {});
465
+ }
466
+
467
+ this.startWorkerSyncLoop();
468
+
469
+ if (this.isHost) {
470
+ if (this.cfCallsReady) {
471
+ await this.initCloudflareCallsSession().catch(() => {
472
+ this.cfCallsReady = false;
473
+ });
474
+ }
475
+ await this.flushWorkerSync();
476
+ } else {
477
+ await this.connectGuestToHost();
478
+ }
479
+ return this;
480
+ }
481
+
482
+ initPeerWithRetry(peerId, attemptsLeft) {
483
+ return new Promise((resolve) => {
484
+ let currentTryId = peerId;
485
+ const peer = new window.Peer(currentTryId, {
486
+ debug: 0,
487
+ pingInterval: 5000,
488
+ config: {
489
+ iceServers: this.iceServers,
490
+ },
491
+ });
492
+
493
+ let settled = false;
494
+ const finish = () => {
495
+ if (!settled) {
496
+ settled = true;
497
+ resolve();
498
+ }
499
+ };
500
+
501
+ const openTimeout = setTimeout(finish, 4500);
502
+
503
+ peer.on("open", (id) => {
504
+ clearTimeout(openTimeout);
505
+ this.peer = peer;
506
+ this.localParticipant.peerId = id || currentTryId;
507
+ this.bindPeerEvents(peer);
508
+ finish();
509
+ });
510
+
511
+ peer.on("error", (err) => {
512
+ if (!this.peer) {
513
+ clearTimeout(openTimeout);
514
+ try {
515
+ peer.destroy();
516
+ } catch {}
517
+ if (err?.type === "unavailable-id" && attemptsLeft > 1) {
518
+ const nextId =
519
+ attemptsLeft === 2 && this.isHost
520
+ ? `${peerId}-${Math.random().toString(36).slice(2, 6)}`
521
+ : peerId;
522
+ setTimeout(() => {
523
+ this.initPeerWithRetry(nextId, attemptsLeft - 1).then(finish);
524
+ }, 600);
525
+ return;
526
+ }
527
+ finish();
528
+ }
529
+ });
530
+ });
531
+ }
532
+
533
+ bindPeerEvents(peer) {
534
+ peer.on("connection", (conn) => {
535
+ this.handleIncomingDataConnection(conn);
536
+ });
537
+
538
+ peer.on("call", (call) => {
539
+ this.handleIncomingMediaCall(call);
540
+ });
541
+
542
+ peer.on("disconnected", () => {
543
+ if (!this.isDisconnected && !peer.destroyed) {
544
+ try {
545
+ peer.reconnect();
546
+ } catch {}
547
+ }
548
+ });
549
+
550
+ if (this.peerKeepAliveTimer) clearInterval(this.peerKeepAliveTimer);
551
+ this.peerKeepAliveTimer = setInterval(() => {
552
+ if (this.isDisconnected) return;
553
+ if (this.peer && this.peer.disconnected && !this.peer.destroyed) {
554
+ try {
555
+ this.peer.reconnect();
556
+ } catch {}
557
+ }
558
+ if (!this.isHost && (!this.hostConn || !this.hostConn.open) && this.peer?.open) {
559
+ this.tryOpenPeerDataChannelToHost();
560
+ }
561
+ }, 5000);
562
+ }
563
+
564
+ enqueueHttpSignal(targetId, packet) {
565
+ const msgId =
566
+ packet?._msgId || `m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
567
+ const enriched = { ...packet, _msgId: msgId, _sentAt: packet?._sentAt || Date.now() };
568
+ this.processedSignalIds.add(msgId);
569
+ this.outgoingHttpSignals.push({
570
+ id: msgId,
571
+ targetId: targetId || "all",
572
+ packet: enriched,
573
+ });
574
+ this.flushWorkerSync();
575
+ return enriched;
576
+ }
577
+
578
+ startWorkerSyncLoop() {
579
+ if (this.syncTimer) clearInterval(this.syncTimer);
580
+ this.syncTimer = setInterval(() => {
581
+ if (!this.isDisconnected) {
582
+ this.flushWorkerSync();
583
+ }
584
+ }, 1300);
585
+ }
586
+
587
+ async flushWorkerSync() {
588
+ if (this.isDisconnected || this.syncInFlight) return;
589
+ this.syncInFlight = true;
590
+
591
+ const batch = [];
592
+ let batchBytes = 0;
593
+ while (this.outgoingHttpSignals.length > 0 && batch.length < 12) {
594
+ const next = this.outgoingHttpSignals[0];
595
+ const size = JSON.stringify(next).length;
596
+ if (batch.length > 0 && batchBytes + size > 48000) break;
597
+ batch.push(this.outgoingHttpSignals.shift());
598
+ batchBytes += size;
599
+ }
600
+
601
+ try {
602
+ const res = await this.apiPost("/api/room-sync", {
603
+ room_hash: this.roomHash,
604
+ identity: this.localParticipant.identity,
605
+ peer_id: this.localParticipant.peerId || "",
606
+ is_host: this.isHost,
607
+ joined_at: this.joinedAt,
608
+ outgoing: batch,
609
+ });
610
+
611
+ if (res?.host_peer_id && res.host_peer_id !== this.knownHostPeerId) {
612
+ this.knownHostPeerId = res.host_peer_id;
613
+ if (!this.isHost && (!this.hostConn || !this.hostConn.open)) {
614
+ this.tryOpenPeerDataChannelToHost();
615
+ }
616
+ }
617
+
618
+ if (Array.isArray(res?.messages)) {
619
+ for (const item of res.messages) {
620
+ if (!item?.id || this.processedSignalIds.has(item.id)) continue;
621
+ this.processedSignalIds.add(item.id);
622
+ const pkt = item.packet;
623
+ if (!pkt || typeof pkt !== "object") continue;
624
+
625
+ if (this.isHost) {
626
+ this.handleSignalingMessageAtHost(null, {
627
+ ...pkt,
628
+ senderId: pkt.senderId || item.senderId,
629
+ });
630
+ } else {
631
+ await this.handleSignalingMessageFromHost(pkt);
632
+ }
633
+ }
634
+ }
635
+ } catch {
636
+ if (batch.length > 0) {
637
+ this.outgoingHttpSignals.unshift(...batch);
638
+ }
639
+ } finally {
640
+ this.syncInFlight = false;
641
+ if (this.outgoingHttpSignals.length > 0 && !this.isDisconnected) {
642
+ setTimeout(() => this.flushWorkerSync(), 120);
643
+ }
644
+ }
645
+ }
646
+
647
+ tryOpenPeerDataChannelToHost() {
648
+ if (this.isDisconnected || this.isHost || !this.peer?.open) return;
649
+ const targetPeerId = this.knownHostPeerId || this.getHostPeerId();
650
+ if (!targetPeerId) return;
651
+
652
+ try {
653
+ const conn = this.peer.connect(targetPeerId, {
654
+ reliable: true,
655
+ serialization: "json",
656
+ });
657
+ if (!conn) return;
658
+
659
+ conn.on("open", () => {
660
+ this.hostConn = conn;
661
+ const helloPkt = {
662
+ type: "hello_guest",
663
+ identity: this.localParticipant.identity,
664
+ name: this.localParticipant.name,
665
+ peerId: this.localParticipant.peerId,
666
+ };
667
+ conn.send(helloPkt);
668
+ });
669
+
670
+ conn.on("data", (msg) => {
671
+ if (msg?._msgId) {
672
+ if (this.processedSignalIds.has(msg._msgId)) return;
673
+ this.processedSignalIds.add(msg._msgId);
674
+ }
675
+ this.handleSignalingMessageFromHost(msg);
676
+ });
677
+
678
+ conn.on("close", () => {
679
+ if (this.hostConn === conn) {
680
+ this.hostConn = null;
681
+ }
682
+ });
683
+ } catch {}
684
+ }
685
+
686
+ async connectGuestToHost() {
687
+ const helloPkt = {
688
+ type: "hello_guest",
689
+ identity: this.localParticipant.identity,
690
+ name: this.localParticipant.name,
691
+ peerId: this.localParticipant.peerId,
692
+ };
693
+
694
+ this.enqueueHttpSignal("host", helloPkt);
695
+ await this.flushWorkerSync();
696
+
697
+ this.tryOpenPeerDataChannelToHost();
698
+
699
+ const helloInterval = setInterval(() => {
700
+ if (this.isDisconnected || this.localParticipant.permissions?.canSubscribe) {
701
+ clearInterval(helloInterval);
702
+ return;
703
+ }
704
+ this.enqueueHttpSignal("host", {
705
+ ...helloPkt,
706
+ peerId: this.localParticipant.peerId,
707
+ });
708
+ if (this.hostConn?.open) {
709
+ try {
710
+ this.hostConn.send({
711
+ ...helloPkt,
712
+ peerId: this.localParticipant.peerId,
713
+ });
714
+ } catch {}
715
+ }
716
+ }, 3200);
717
+ }
718
+
719
+ handleIncomingDataConnection(conn) {
720
+ conn.on("data", (msg) => {
721
+ if (!msg || typeof msg !== "object") return;
722
+ if (msg._msgId) {
723
+ if (this.processedSignalIds.has(msg._msgId)) return;
724
+ this.processedSignalIds.add(msg._msgId);
725
+ }
726
+
727
+ if (this.isHost) {
728
+ this.handleSignalingMessageAtHost(conn, msg);
729
+ } else {
730
+ this.handleSignalingMessageFromHost(msg);
731
+ }
732
+ });
733
+
734
+ conn.on("close", () => {
735
+ if (this.isHost && conn._guestIdentity) {
736
+ this.guestConns.delete(conn._guestIdentity);
737
+ }
738
+ });
739
+ }
740
+
741
+ handleSignalingMessageAtHost(conn, msg) {
742
+ if (msg.type === "hello_guest") {
743
+ const guestId = msg.identity;
744
+ if (!guestId) return;
745
+ if (conn) {
746
+ conn._guestIdentity = guestId;
747
+ this.guestConns.set(guestId, conn);
748
+ }
749
+
750
+ const existing = this.remoteParticipants.get(guestId);
751
+ if (existing) {
752
+ if (msg.peerId) existing.peerId = msg.peerId;
753
+ if (msg.name) existing.name = msg.name;
754
+ if (existing.permissions?.canSubscribe) {
755
+ const roster = this.buildApprovedRosterList();
756
+ const approvePkt = this.enqueueHttpSignal(guestId, {
757
+ type: "decision",
758
+ action: "approve",
759
+ roomTitle: this.roomTitle,
760
+ roster,
761
+ });
762
+ if (conn?.open) conn.send(approvePkt);
763
+ }
764
+ return;
765
+ }
766
+
767
+ const guestParticipant = new HybridParticipant({
768
+ identity: guestId,
769
+ name: msg.name || "Invitado",
770
+ metadata: JSON.stringify({ role: "guest", status: "waiting" }),
771
+ permissions: {
772
+ canPublish: false,
773
+ canSubscribe: false,
774
+ canPublishData: false,
775
+ },
776
+ isLocal: false,
777
+ room: this,
778
+ });
779
+ guestParticipant.peerId = msg.peerId || "";
780
+
781
+ this.remoteParticipants.set(guestId, guestParticipant);
782
+ this.emit(RoomEvents.ParticipantConnected, guestParticipant);
783
+ return;
784
+ }
785
+
786
+ const senderId = conn?._guestIdentity || msg.identity || msg.senderId;
787
+ const senderP = this.remoteParticipants.get(senderId);
788
+ if (!senderP) return;
789
+
790
+ if (msg.type === "media_state") {
791
+ this.applyRemoteMediaState(senderP, msg);
792
+ this.broadcastRoster();
793
+ return;
794
+ }
795
+
796
+ if (msg.type === "rtc_signal") {
797
+ const targetId = msg.targetId;
798
+ if (!targetId || targetId === this.localParticipant.identity) {
799
+ this.handleRtcSignal(senderId, senderP.name || msg.senderName, msg);
800
+ } else {
801
+ const targetConn = this.guestConns.get(targetId);
802
+ const targetP = this.remoteParticipants.get(targetId);
803
+ const forwarded = {
804
+ ...msg,
805
+ senderId,
806
+ senderName: senderP.name || msg.senderName,
807
+ };
808
+ this.enqueueHttpSignal(targetId, forwarded);
809
+ if (targetConn?.open && targetP?.permissions?.canSubscribe) {
810
+ targetConn.send(forwarded);
811
+ }
812
+ }
813
+ return;
814
+ }
815
+
816
+ if (msg.type === "app_data") {
817
+ this.deliverAppData(msg.senderId, msg.senderName, msg);
818
+ this.enqueueHttpSignal("all", msg);
819
+ for (const [id, gConn] of this.guestConns.entries()) {
820
+ if (id !== senderId && gConn.open) {
821
+ const gp = this.remoteParticipants.get(id);
822
+ if (gp?.permissions?.canSubscribe) {
823
+ gConn.send(msg);
824
+ }
825
+ }
826
+ }
827
+ }
828
+ }
829
+
830
+ async handleSignalingMessageFromHost(msg) {
831
+ if (!msg || typeof msg !== "object") return;
832
+
833
+ if (msg.type === "decision") {
834
+ if (msg.action === "reject") {
835
+ this.disconnect();
836
+ this.emit(RoomEvents.Disconnected);
837
+ return;
838
+ }
839
+
840
+ if (msg.action === "approve") {
841
+ if (msg.roomTitle) {
842
+ this.roomTitle = msg.roomTitle;
843
+ }
844
+ this.localParticipant.metadata = JSON.stringify({
845
+ role: "guest",
846
+ status: "approved",
847
+ room_title: this.roomTitle,
848
+ });
849
+ this.localParticipant.permissions = {
850
+ canPublish: true,
851
+ canSubscribe: true,
852
+ canPublishData: true,
853
+ };
854
+
855
+ if (this.cfCallsReady) {
856
+ await this.initCloudflareCallsSession().catch(() => {
857
+ this.cfCallsReady = false;
858
+ });
859
+ }
860
+
861
+ if (Array.isArray(msg.roster)) {
862
+ await this.syncRosterFromHost(msg.roster);
863
+ }
864
+
865
+ this.emit(RoomEvents.ParticipantMetadataChanged, this.localParticipant);
866
+ this.emit(RoomEvents.ParticipantPermissionsChanged, this.localParticipant);
867
+ }
868
+ return;
869
+ }
870
+
871
+ if (msg.type === "roster") {
872
+ if (Array.isArray(msg.roster)) {
873
+ await this.syncRosterFromHost(msg.roster);
874
+ }
875
+ return;
876
+ }
877
+
878
+ if (msg.type === "rtc_signal") {
879
+ this.handleRtcSignal(msg.senderId, msg.senderName, msg);
880
+ return;
881
+ }
882
+
883
+ if (msg.type === "app_data") {
884
+ this.deliverAppData(msg.senderId, msg.senderName, msg);
885
+ return;
886
+ }
887
+
888
+ if (msg.type === "kicked" || msg.type === "room_ended") {
889
+ if (msg._sentAt && msg._sentAt < this.joinedAt - 3000) return;
890
+ this.disconnect();
891
+ this.emit(RoomEvents.Disconnected);
892
+ }
893
+ }
894
+
895
+ async deliverAppData(senderId, senderName, msgPacket) {
896
+ if (!msgPacket || senderId === this.localParticipant.identity) return;
897
+ let plaintext = "";
898
+ if (msgPacket.cipher) {
899
+ try {
900
+ plaintext = await decryptDataPacket(this.roomHash, msgPacket.cipher);
901
+ } catch {
902
+ return;
903
+ }
904
+ } else if (typeof msgPacket.text === "string") {
905
+ plaintext = msgPacket.text;
906
+ }
907
+ if (!plaintext) return;
908
+
909
+ const participant = this.remoteParticipants.get(senderId) || {
910
+ identity: senderId,
911
+ name: senderName || "Participante",
912
+ };
913
+ const payload = new TextEncoder().encode(plaintext);
914
+ this.emit(RoomEvents.DataReceived, payload, plaintext, participant);
915
+ }
916
+
917
+ async admitGuest(identity) {
918
+ if (!this.isHost) return;
919
+ try {
920
+ await this.apiPost("/api/decide", {
921
+ room_hash: this.roomHash,
922
+ host_secret: this.hostSecret,
923
+ identity,
924
+ action: "approve",
925
+ });
926
+ } catch {}
927
+
928
+ const conn = this.guestConns.get(identity);
929
+ const guestP = this.remoteParticipants.get(identity);
930
+ if (!guestP) return;
931
+
932
+ guestP.metadata = JSON.stringify({
933
+ role: "guest",
934
+ status: "approved",
935
+ room_title: this.roomTitle,
936
+ });
937
+ guestP.permissions = {
938
+ canPublish: true,
939
+ canSubscribe: true,
940
+ canPublishData: true,
941
+ };
942
+
943
+ const roster = this.buildApprovedRosterList();
944
+ const approvePkt = this.enqueueHttpSignal(identity, {
945
+ type: "decision",
946
+ action: "approve",
947
+ roomTitle: this.roomTitle,
948
+ roster,
949
+ });
950
+ if (conn?.open) {
951
+ conn.send(approvePkt);
952
+ }
953
+
954
+ this.emit(RoomEvents.ParticipantMetadataChanged, guestP);
955
+ this.broadcastRoster();
956
+
957
+ if (!this.cfCallsReady) {
958
+ this.startDirectUserMediaToPeer(guestP.identity);
959
+ if (guestP.peerId) {
960
+ this.callPeerWithLocalTracks(guestP.peerId, guestP.identity);
961
+ }
962
+ } else if (this.getLocalScreenMediaStream()) {
963
+ this.startDirectScreenShareToPeer(guestP.identity);
964
+ if (guestP.peerId) {
965
+ this.callPeerScreenShare(guestP.peerId, guestP.identity);
966
+ }
967
+ }
968
+ }
969
+
970
+ async rejectGuest(identity) {
971
+ if (!this.isHost) return;
972
+ try {
973
+ await this.apiPost("/api/decide", {
974
+ room_hash: this.roomHash,
975
+ host_secret: this.hostSecret,
976
+ identity,
977
+ action: "reject",
978
+ });
979
+ } catch {}
980
+
981
+ const conn = this.guestConns.get(identity);
982
+ const guestP = this.remoteParticipants.get(identity);
983
+ if (!guestP) return;
984
+
985
+ const rejectPkt = this.enqueueHttpSignal(identity, {
986
+ type: "decision",
987
+ action: "reject",
988
+ });
989
+ if (conn?.open) {
990
+ conn.send(rejectPkt);
991
+ setTimeout(() => conn.close(), 200);
992
+ }
993
+ this.guestConns.delete(identity);
994
+ this.remoteParticipants.delete(identity);
995
+ this.emit(RoomEvents.ParticipantDisconnected, guestP);
996
+ }
997
+
998
+ buildParticipantSummary(p) {
999
+ const micPub = p.getTrackPublication(TrackSources.Microphone);
1000
+ const camPub = p.getTrackPublication(TrackSources.Camera);
1001
+ const scrPub = p.getTrackPublication(TrackSources.ScreenShare);
1002
+ return {
1003
+ identity: p.identity,
1004
+ name: p.name,
1005
+ peerId: p.peerId,
1006
+ metadata: p.metadata,
1007
+ micMuted: !micPub || micPub.isMuted,
1008
+ camMuted: !camPub || camPub.isMuted,
1009
+ hasScreen: Boolean(scrPub && !scrPub.isMuted),
1010
+ cfSessionId: p.cfSessionId || "",
1011
+ cfPublishedTracks: p.cfPublishedTracks || [],
1012
+ };
1013
+ }
1014
+
1015
+ buildApprovedRosterList() {
1016
+ const list = [this.buildParticipantSummary(this.localParticipant)];
1017
+ for (const p of this.remoteParticipants.values()) {
1018
+ if (p.permissions?.canSubscribe) {
1019
+ list.push(this.buildParticipantSummary(p));
1020
+ }
1021
+ }
1022
+ return list;
1023
+ }
1024
+
1025
+ broadcastRoster() {
1026
+ if (!this.isHost) return;
1027
+ const roster = this.buildApprovedRosterList();
1028
+ const rosterPkt = this.enqueueHttpSignal("all", { type: "roster", roster });
1029
+ for (const [id, conn] of this.guestConns.entries()) {
1030
+ const gp = this.remoteParticipants.get(id);
1031
+ if (conn.open && gp?.permissions?.canSubscribe) {
1032
+ conn.send(rosterPkt);
1033
+ }
1034
+ }
1035
+ if (this.cfCallsReady) {
1036
+ for (const p of this.remoteParticipants.values()) {
1037
+ if (p.permissions?.canSubscribe && p.cfSessionId) {
1038
+ this.pullRemoteTracksFromCloudflare(p);
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ async syncRosterFromHost(roster) {
1045
+ const incomingIds = new Set();
1046
+ let changed = false;
1047
+
1048
+ for (const item of roster) {
1049
+ if (item.identity === this.localParticipant.identity) continue;
1050
+ incomingIds.add(item.identity);
1051
+
1052
+ let p = this.remoteParticipants.get(item.identity);
1053
+ const isNew = !p;
1054
+ if (!p) {
1055
+ p = new HybridParticipant({
1056
+ identity: item.identity,
1057
+ name: item.name,
1058
+ metadata: item.metadata || JSON.stringify({ role: "guest", status: "approved" }),
1059
+ permissions: {
1060
+ canPublish: true,
1061
+ canSubscribe: true,
1062
+ canPublishData: true,
1063
+ },
1064
+ isLocal: false,
1065
+ room: this,
1066
+ });
1067
+ this.remoteParticipants.set(item.identity, p);
1068
+ changed = true;
1069
+ }
1070
+
1071
+ if (item.name && p.name !== item.name) {
1072
+ p.name = item.name;
1073
+ changed = true;
1074
+ }
1075
+ if (item.peerId && p.peerId !== item.peerId) {
1076
+ p.peerId = item.peerId;
1077
+ }
1078
+ if (item.metadata && p.metadata !== item.metadata) {
1079
+ p.metadata = item.metadata;
1080
+ changed = true;
1081
+ }
1082
+ const mediaChanged = this.applyRemoteMediaState(p, item, false);
1083
+ if (mediaChanged) changed = true;
1084
+
1085
+ if (isNew && !this.cfCallsReady) {
1086
+ this.startDirectUserMediaToPeer(p.identity);
1087
+ if (p.peerId) {
1088
+ this.callPeerWithLocalTracks(p.peerId, p.identity);
1089
+ }
1090
+ }
1091
+
1092
+ if (isNew && this.getLocalScreenMediaStream()) {
1093
+ this.startDirectScreenShareToPeer(p.identity);
1094
+ }
1095
+
1096
+ if (this.cfCallsReady && p.cfSessionId) {
1097
+ await this.pullRemoteTracksFromCloudflare(p);
1098
+ }
1099
+ }
1100
+
1101
+ for (const [id, p] of this.remoteParticipants.entries()) {
1102
+ if (!incomingIds.has(id)) {
1103
+ this.cleanupRemoteParticipantTracks(p);
1104
+ this.remoteParticipants.delete(id);
1105
+ changed = true;
1106
+ }
1107
+ }
1108
+
1109
+ if (changed) {
1110
+ this.emit(RoomEvents.ParticipantConnected);
1111
+ }
1112
+ }
1113
+
1114
+ applyRemoteMediaState(participant, state, shouldEmit = true) {
1115
+ let changed = false;
1116
+ if (typeof state.cfSessionId === "string" && participant.cfSessionId !== state.cfSessionId) {
1117
+ participant.cfSessionId = state.cfSessionId;
1118
+ changed = true;
1119
+ }
1120
+ if (Array.isArray(state.cfPublishedTracks)) {
1121
+ participant.cfPublishedTracks = state.cfPublishedTracks;
1122
+ }
1123
+
1124
+ const micPub = participant.getTrackPublication(TrackSources.Microphone);
1125
+ if (micPub && typeof state.micMuted === "boolean" && micPub.isMuted !== state.micMuted) {
1126
+ micPub.isMuted = state.micMuted;
1127
+ changed = true;
1128
+ }
1129
+
1130
+ const camPub = participant.getTrackPublication(TrackSources.Camera);
1131
+ if (camPub && typeof state.camMuted === "boolean" && camPub.isMuted !== state.camMuted) {
1132
+ camPub.isMuted = state.camMuted;
1133
+ changed = true;
1134
+ }
1135
+
1136
+ if (state.hasScreen === false) {
1137
+ const scrPub = participant.getTrackPublication(TrackSources.ScreenShare);
1138
+ if (scrPub) {
1139
+ participant.setPublication(TrackSources.ScreenShare, null);
1140
+ this.emit(RoomEvents.TrackUnpublished, scrPub, participant);
1141
+ if (scrPub.track) {
1142
+ this.emit(RoomEvents.TrackUnsubscribed, scrPub.track, scrPub, participant);
1143
+ }
1144
+ changed = true;
1145
+ }
1146
+ }
1147
+
1148
+ if (this.isHost && this.cfCallsReady && participant.cfSessionId) {
1149
+ this.pullRemoteTracksFromCloudflare(participant);
1150
+ }
1151
+ if (shouldEmit && changed) {
1152
+ this.emit(RoomEvents.TrackMuted);
1153
+ }
1154
+ return changed;
1155
+ }
1156
+
1157
+ sendLocalMediaState() {
1158
+ const summary = this.buildParticipantSummary(this.localParticipant);
1159
+ if (this.isHost) {
1160
+ this.broadcastRoster();
1161
+ } else {
1162
+ const pkt = this.enqueueHttpSignal("host", {
1163
+ type: "media_state",
1164
+ ...summary,
1165
+ });
1166
+ if (this.hostConn?.open) {
1167
+ this.hostConn.send(pkt);
1168
+ }
1169
+ }
1170
+ }
1171
+
1172
+ async enableCameraAndMicrophone() {
1173
+ const stream = await navigator.mediaDevices.getUserMedia({
1174
+ video: { width: { ideal: 1280 }, height: { ideal: 720 } },
1175
+ audio: {
1176
+ echoCancellation: true,
1177
+ noiseSuppression: true,
1178
+ autoGainControl: true,
1179
+ },
1180
+ });
1181
+
1182
+ const audioTrack = stream.getAudioTracks()[0];
1183
+ const videoTrack = stream.getVideoTracks()[0];
1184
+
1185
+ if (audioTrack) {
1186
+ this.setLocalPublicationTrack(TrackSources.Microphone, audioTrack, TrackKinds.Audio);
1187
+ }
1188
+ if (videoTrack) {
1189
+ this.setLocalPublicationTrack(TrackSources.Camera, videoTrack, TrackKinds.Video);
1190
+ }
1191
+
1192
+ await this.syncOutgoingMedia();
1193
+ }
1194
+
1195
+ async setMicrophoneEnabled(enabled) {
1196
+ let micPub = this.localParticipant.getTrackPublication(TrackSources.Microphone);
1197
+ if (enabled) {
1198
+ if (!micPub?.track?.mediaStreamTrack || micPub.track.mediaStreamTrack.readyState === "ended") {
1199
+ const stream = await navigator.mediaDevices.getUserMedia({
1200
+ audio: {
1201
+ echoCancellation: true,
1202
+ noiseSuppression: true,
1203
+ autoGainControl: true,
1204
+ },
1205
+ });
1206
+ const audioTrack = stream.getAudioTracks()[0];
1207
+ this.setLocalPublicationTrack(TrackSources.Microphone, audioTrack, TrackKinds.Audio);
1208
+ } else {
1209
+ micPub.track.mediaStreamTrack.enabled = true;
1210
+ micPub.isMuted = false;
1211
+ }
1212
+ } else if (micPub) {
1213
+ if (micPub.track?.mediaStreamTrack) {
1214
+ micPub.track.mediaStreamTrack.enabled = false;
1215
+ }
1216
+ micPub.isMuted = true;
1217
+ }
1218
+
1219
+ await this.syncOutgoingMedia();
1220
+ this.emit(enabled ? RoomEvents.TrackUnmuted : RoomEvents.TrackMuted, micPub);
1221
+ }
1222
+
1223
+ async setCameraEnabled(enabled) {
1224
+ let camPub = this.localParticipant.getTrackPublication(TrackSources.Camera);
1225
+ if (enabled) {
1226
+ if (!camPub?.track?.mediaStreamTrack || camPub.track.mediaStreamTrack.readyState === "ended") {
1227
+ const stream = await navigator.mediaDevices.getUserMedia({
1228
+ video: { width: { ideal: 1280 }, height: { ideal: 720 } },
1229
+ });
1230
+ const videoTrack = stream.getVideoTracks()[0];
1231
+ this.setLocalPublicationTrack(TrackSources.Camera, videoTrack, TrackKinds.Video);
1232
+ } else {
1233
+ camPub.track.mediaStreamTrack.enabled = true;
1234
+ camPub.isMuted = false;
1235
+ }
1236
+ } else if (camPub) {
1237
+ if (camPub.track?.mediaStreamTrack) {
1238
+ camPub.track.mediaStreamTrack.stop();
1239
+ }
1240
+ camPub.isMuted = true;
1241
+ }
1242
+
1243
+ await this.syncOutgoingMedia();
1244
+ this.emit(enabled ? RoomEvents.TrackUnmuted : RoomEvents.TrackMuted, camPub);
1245
+ }
1246
+
1247
+ async setScreenShareEnabled(enabled, stopTracks = true) {
1248
+ const existingPub = this.localParticipant.getTrackPublication(TrackSources.ScreenShare);
1249
+ if (!enabled) {
1250
+ if (existingPub) {
1251
+ if (stopTracks && existingPub.track) {
1252
+ existingPub.track.stop();
1253
+ }
1254
+ this.localParticipant.setPublication(TrackSources.ScreenShare, null);
1255
+ this.closeOutgoingScreenCalls();
1256
+ if (this.cfCallsReady) {
1257
+ const prevTr = this.cfTransceivers.get(this.currentScreenTrackName);
1258
+ if (prevTr?.sender) {
1259
+ prevTr.sender.replaceTrack(null).catch(() => {});
1260
+ }
1261
+ this.localParticipant.cfPublishedTracks = (
1262
+ this.localParticipant.cfPublishedTracks || []
1263
+ ).filter((t) => t !== "screen" && !t.startsWith("screen_"));
1264
+ }
1265
+ this.sendLocalMediaState();
1266
+ this.emit(RoomEvents.LocalTrackUnpublished, existingPub);
1267
+ }
1268
+ return null;
1269
+ }
1270
+
1271
+ const stream = await navigator.mediaDevices.getDisplayMedia({
1272
+ video: { frameRate: { ideal: 30, max: 30 } },
1273
+ audio: false,
1274
+ });
1275
+ const screenVideoTrack = stream.getVideoTracks()[0];
1276
+ if (!screenVideoTrack) return null;
1277
+
1278
+ this.currentScreenTrackName = `screen_${Date.now().toString(36).slice(-4)}`;
1279
+
1280
+ const pub = this.setLocalPublicationTrack(
1281
+ TrackSources.ScreenShare,
1282
+ screenVideoTrack,
1283
+ TrackKinds.Video
1284
+ );
1285
+
1286
+ await this.syncOutgoingMedia(true);
1287
+ this.emit(RoomEvents.LocalTrackPublished, pub);
1288
+ return pub;
1289
+ }
1290
+
1291
+ async startScreenShare() {
1292
+ return this.setScreenShareEnabled(true);
1293
+ }
1294
+
1295
+ async stopScreenShare() {
1296
+ return this.setScreenShareEnabled(false);
1297
+ }
1298
+
1299
+ setLocalPublicationTrack(source, mediaStreamTrack, kind) {
1300
+ const hTrack = new HybridTrack(mediaStreamTrack, kind, source);
1301
+ const pub = new HybridPublication({
1302
+ trackSid: `${source}_${this.localParticipant.identity}`,
1303
+ source,
1304
+ track: hTrack,
1305
+ isMuted: false,
1306
+ });
1307
+ this.localParticipant.setPublication(source, pub);
1308
+ return pub;
1309
+ }
1310
+
1311
+ async syncOutgoingMedia(includeScreen = false) {
1312
+ if (includeScreen) {
1313
+ this.callAllPeersScreenShare();
1314
+ }
1315
+
1316
+ if (this.cfCallsReady) {
1317
+ try {
1318
+ await this.pushLocalTracksToCloudflare();
1319
+ this.sendLocalMediaState();
1320
+ if (includeScreen) {
1321
+ setTimeout(() => {
1322
+ if (!this.isDisconnected) this.sendLocalMediaState();
1323
+ }, 650);
1324
+ }
1325
+ return;
1326
+ } catch {
1327
+ this.cfCallsReady = false;
1328
+ }
1329
+ }
1330
+
1331
+ if (!includeScreen) {
1332
+ for (const p of this.remoteParticipants.values()) {
1333
+ if (p.permissions?.canSubscribe) {
1334
+ this.startDirectUserMediaToPeer(p.identity);
1335
+ if (p.peerId) {
1336
+ this.callPeerWithLocalTracks(p.peerId, p.identity, false);
1337
+ }
1338
+ }
1339
+ }
1340
+ }
1341
+ this.sendLocalMediaState();
1342
+ }
1343
+
1344
+ sendSignalingToParticipant(targetIdentity, packet) {
1345
+ const payload = {
1346
+ ...packet,
1347
+ targetId: targetIdentity,
1348
+ senderId: this.localParticipant.identity,
1349
+ senderName: this.localParticipant.name,
1350
+ };
1351
+
1352
+ const enriched = this.enqueueHttpSignal(targetIdentity, payload);
1353
+ if (this.isHost) {
1354
+ const conn = this.guestConns.get(targetIdentity);
1355
+ if (conn?.open) {
1356
+ conn.send(enriched);
1357
+ }
1358
+ } else if (this.hostConn?.open) {
1359
+ this.hostConn.send(enriched);
1360
+ }
1361
+ }
1362
+
1363
+ async startDirectUserMediaToPeer(remoteIdentity) {
1364
+ if (!remoteIdentity || this.cfCallsReady) return;
1365
+ const userStream = this.getLocalUserMediaStream();
1366
+ if (!userStream) return;
1367
+
1368
+ const pcKey = `${remoteIdentity}:user`;
1369
+ const existingPc = this.directScreenPcs.get(pcKey);
1370
+ const isAlive = Boolean(
1371
+ existingPc &&
1372
+ existingPc.connectionState !== "closed" &&
1373
+ existingPc.connectionState !== "failed" &&
1374
+ existingPc.iceConnectionState !== "closed" &&
1375
+ existingPc.iceConnectionState !== "failed"
1376
+ );
1377
+
1378
+ if (isAlive && typeof existingPc.getSenders === "function") {
1379
+ const senders = existingPc.getSenders();
1380
+ const newAudio = userStream.getAudioTracks()[0] || null;
1381
+ const newVideo = userStream.getVideoTracks()[0] || null;
1382
+ const audioSender = senders.find((s) => s.track?.kind === "audio");
1383
+ const videoSender = senders.find((s) => s.track?.kind === "video");
1384
+ const canReplaceAudio = !newAudio || Boolean(audioSender);
1385
+ const canReplaceVideo = !newVideo || Boolean(videoSender);
1386
+ if (canReplaceAudio && canReplaceVideo) {
1387
+ if (audioSender && newAudio) audioSender.replaceTrack(newAudio).catch(() => {});
1388
+ if (videoSender) videoSender.replaceTrack(newVideo).catch(() => {});
1389
+ return;
1390
+ }
1391
+ }
1392
+
1393
+ if (existingPc) {
1394
+ try {
1395
+ existingPc.close();
1396
+ } catch {}
1397
+ this.directScreenPcs.delete(pcKey);
1398
+ }
1399
+ this.pendingScreenIce.delete(`${pcKey}:caller`);
1400
+
1401
+ const pc = new RTCPeerConnection({
1402
+ iceServers: this.iceServers,
1403
+ });
1404
+ this.directScreenPcs.set(pcKey, pc);
1405
+
1406
+ pc.onicecandidate = (ev) => {
1407
+ if (ev.candidate && this.directScreenPcs.get(pcKey) === pc) {
1408
+ this.sendSignalingToParticipant(remoteIdentity, {
1409
+ type: "rtc_signal",
1410
+ streamKind: "user",
1411
+ signalType: "candidate",
1412
+ role: "caller",
1413
+ candidate: ev.candidate.toJSON ? ev.candidate.toJSON() : ev.candidate,
1414
+ });
1415
+ }
1416
+ };
1417
+
1418
+ for (const track of userStream.getTracks()) {
1419
+ track.enabled = true;
1420
+ pc.addTrack(track, userStream);
1421
+ }
1422
+
1423
+ try {
1424
+ const offer = await pc.createOffer();
1425
+ await pc.setLocalDescription(offer);
1426
+ this.sendSignalingToParticipant(remoteIdentity, {
1427
+ type: "rtc_signal",
1428
+ streamKind: "user",
1429
+ signalType: "offer",
1430
+ sdp: {
1431
+ type: offer.type,
1432
+ sdp: offer.sdp,
1433
+ },
1434
+ });
1435
+ } catch {}
1436
+ }
1437
+
1438
+ async startDirectScreenShareToPeer(remoteIdentity) {
1439
+ if (!remoteIdentity) return;
1440
+ const screenStream = this.getLocalScreenMediaStream();
1441
+ const screenTrack = screenStream?.getVideoTracks()?.[0];
1442
+ if (!screenStream || !screenTrack) return;
1443
+
1444
+ screenTrack.enabled = true;
1445
+ const pcKey = `${remoteIdentity}:screen`;
1446
+ const prevPc = this.directScreenPcs.get(pcKey);
1447
+ if (prevPc) {
1448
+ try {
1449
+ prevPc.close();
1450
+ } catch {}
1451
+ this.directScreenPcs.delete(pcKey);
1452
+ }
1453
+ this.pendingScreenIce.delete(`${pcKey}:caller`);
1454
+
1455
+ const pc = new RTCPeerConnection({
1456
+ iceServers: this.iceServers,
1457
+ });
1458
+ this.directScreenPcs.set(pcKey, pc);
1459
+
1460
+ pc.onicecandidate = (ev) => {
1461
+ if (ev.candidate && this.directScreenPcs.get(pcKey) === pc) {
1462
+ this.sendSignalingToParticipant(remoteIdentity, {
1463
+ type: "rtc_signal",
1464
+ streamKind: "screen",
1465
+ signalType: "candidate",
1466
+ role: "caller",
1467
+ candidate: ev.candidate.toJSON ? ev.candidate.toJSON() : ev.candidate,
1468
+ });
1469
+ }
1470
+ };
1471
+
1472
+ pc.addTrack(screenTrack, screenStream);
1473
+
1474
+ try {
1475
+ const offer = await pc.createOffer();
1476
+ await pc.setLocalDescription(offer);
1477
+ this.sendSignalingToParticipant(remoteIdentity, {
1478
+ type: "rtc_signal",
1479
+ streamKind: "screen",
1480
+ signalType: "offer",
1481
+ sdp: {
1482
+ type: offer.type,
1483
+ sdp: offer.sdp,
1484
+ },
1485
+ });
1486
+ } catch {}
1487
+ }
1488
+
1489
+ handleRtcSignal(senderId, senderName, msg) {
1490
+ if (!senderId || !msg) return Promise.resolve();
1491
+ const streamKind = msg.streamKind === "user" ? "user" : "screen";
1492
+ const pcKey = `${senderId}:${streamKind}`;
1493
+ const prevChain = this.rtcSignalQueues.get(pcKey) || Promise.resolve();
1494
+ const nextChain = prevChain
1495
+ .then(() => this._processRtcSignalSequential(senderId, senderName, msg, streamKind, pcKey))
1496
+ .catch(() => {});
1497
+ this.rtcSignalQueues.set(pcKey, nextChain);
1498
+ return nextChain;
1499
+ }
1500
+
1501
+ async _processRtcSignalSequential(senderId, senderName, msg, streamKind, pcKey) {
1502
+ let participant = this.remoteParticipants.get(senderId);
1503
+ if (!participant) {
1504
+ participant = new HybridParticipant({
1505
+ identity: senderId,
1506
+ name: senderName || "Participante",
1507
+ metadata: JSON.stringify({ role: "guest", status: "approved" }),
1508
+ permissions: { canPublish: true, canSubscribe: true, canPublishData: true },
1509
+ isLocal: false,
1510
+ room: this,
1511
+ });
1512
+ this.remoteParticipants.set(senderId, participant);
1513
+ }
1514
+
1515
+ const attachIncomingTrack = (track) => {
1516
+ if (!track) return;
1517
+ if (streamKind === "screen") {
1518
+ if (track.kind === "video") {
1519
+ this.attachRemoteTrackToParticipant(
1520
+ participant,
1521
+ track,
1522
+ TrackKinds.Video,
1523
+ TrackSources.ScreenShare
1524
+ );
1525
+ }
1526
+ } else if (track.kind === "audio") {
1527
+ this.attachRemoteTrackToParticipant(
1528
+ participant,
1529
+ track,
1530
+ TrackKinds.Audio,
1531
+ TrackSources.Microphone
1532
+ );
1533
+ } else if (track.kind === "video") {
1534
+ this.attachRemoteTrackToParticipant(
1535
+ participant,
1536
+ track,
1537
+ TrackKinds.Video,
1538
+ TrackSources.Camera
1539
+ );
1540
+ }
1541
+ };
1542
+
1543
+ if (msg.signalType === "offer" && msg.sdp) {
1544
+ const prevPc = this.incomingScreenPcs.get(pcKey);
1545
+ if (prevPc) {
1546
+ const prevRemoteSdp =
1547
+ prevPc.currentRemoteDescription?.sdp || prevPc.remoteDescription?.sdp || "";
1548
+ if (
1549
+ prevRemoteSdp &&
1550
+ prevRemoteSdp === msg.sdp.sdp &&
1551
+ prevPc.connectionState !== "closed" &&
1552
+ prevPc.connectionState !== "failed"
1553
+ ) {
1554
+ return;
1555
+ }
1556
+ try {
1557
+ prevPc.close();
1558
+ } catch {}
1559
+ }
1560
+
1561
+ const pc = new RTCPeerConnection({
1562
+ iceServers: this.iceServers,
1563
+ });
1564
+ this.incomingScreenPcs.set(pcKey, pc);
1565
+
1566
+ pc.ontrack = (ev) => {
1567
+ if (ev.track) {
1568
+ attachIncomingTrack(ev.track);
1569
+ } else if (ev.streams?.[0]) {
1570
+ for (const t of ev.streams[0].getTracks()) {
1571
+ attachIncomingTrack(t);
1572
+ }
1573
+ }
1574
+ };
1575
+
1576
+ pc.onicecandidate = (ev) => {
1577
+ if (ev.candidate && this.incomingScreenPcs.get(pcKey) === pc) {
1578
+ this.sendSignalingToParticipant(senderId, {
1579
+ type: "rtc_signal",
1580
+ streamKind,
1581
+ signalType: "candidate",
1582
+ role: "callee",
1583
+ candidate: ev.candidate.toJSON ? ev.candidate.toJSON() : ev.candidate,
1584
+ });
1585
+ }
1586
+ };
1587
+
1588
+ try {
1589
+ await pc.setRemoteDescription(new RTCSessionDescription(msg.sdp));
1590
+
1591
+ for (const tr of pc.getTransceivers()) {
1592
+ if (tr.direction === "inactive" || tr.direction === "sendonly") {
1593
+ try {
1594
+ tr.direction = "recvonly";
1595
+ } catch {}
1596
+ }
1597
+ if (tr.receiver?.track) {
1598
+ attachIncomingTrack(tr.receiver.track);
1599
+ }
1600
+ }
1601
+
1602
+ const queuedKey = `${pcKey}:callee`;
1603
+ const queued = this.pendingScreenIce.get(queuedKey) || [];
1604
+ this.pendingScreenIce.delete(queuedKey);
1605
+ for (const cand of queued) {
1606
+ await pc.addIceCandidate(new RTCIceCandidate(cand)).catch(() => {});
1607
+ }
1608
+
1609
+ const answer = await pc.createAnswer();
1610
+ await pc.setLocalDescription(answer);
1611
+ this.sendSignalingToParticipant(senderId, {
1612
+ type: "rtc_signal",
1613
+ streamKind,
1614
+ signalType: "answer",
1615
+ sdp: {
1616
+ type: answer.type,
1617
+ sdp: answer.sdp,
1618
+ },
1619
+ });
1620
+ } catch {}
1621
+ return;
1622
+ }
1623
+
1624
+ if (msg.signalType === "answer" && msg.sdp) {
1625
+ const pc = this.directScreenPcs.get(pcKey);
1626
+ if (!pc || pc.signalingState === "stable") return;
1627
+ try {
1628
+ await pc.setRemoteDescription(new RTCSessionDescription(msg.sdp));
1629
+ const queuedKey = `${pcKey}:caller`;
1630
+ const queued = this.pendingScreenIce.get(queuedKey) || [];
1631
+ this.pendingScreenIce.delete(queuedKey);
1632
+ for (const cand of queued) {
1633
+ await pc.addIceCandidate(new RTCIceCandidate(cand)).catch(() => {});
1634
+ }
1635
+ } catch {}
1636
+ return;
1637
+ }
1638
+
1639
+ if (msg.signalType === "candidate" && msg.candidate) {
1640
+ const isFromCaller = msg.role === "caller";
1641
+ const targetPc = isFromCaller
1642
+ ? this.incomingScreenPcs.get(pcKey)
1643
+ : this.directScreenPcs.get(pcKey);
1644
+ const queueKey = `${pcKey}:${isFromCaller ? "callee" : "caller"}`;
1645
+
1646
+ if (targetPc && targetPc.remoteDescription) {
1647
+ await targetPc.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(() => {});
1648
+ } else {
1649
+ if (!this.pendingScreenIce.has(queueKey)) {
1650
+ this.pendingScreenIce.set(queueKey, []);
1651
+ }
1652
+ this.pendingScreenIce.get(queueKey).push(msg.candidate);
1653
+ }
1654
+ }
1655
+ }
1656
+
1657
+ getLocalUserMediaStream() {
1658
+ const tracks = [];
1659
+ const micPub = this.localParticipant.getTrackPublication(TrackSources.Microphone);
1660
+ const camPub = this.localParticipant.getTrackPublication(TrackSources.Camera);
1661
+
1662
+ if (micPub?.track?.mediaStreamTrack && micPub.track.mediaStreamTrack.readyState === "live") {
1663
+ tracks.push(micPub.track.mediaStreamTrack);
1664
+ }
1665
+ if (
1666
+ camPub?.track?.mediaStreamTrack &&
1667
+ !camPub.isMuted &&
1668
+ camPub.track.mediaStreamTrack.readyState === "live"
1669
+ ) {
1670
+ tracks.push(camPub.track.mediaStreamTrack);
1671
+ }
1672
+ return tracks.length > 0 ? new MediaStream(tracks) : null;
1673
+ }
1674
+
1675
+ getLocalScreenMediaStream() {
1676
+ const scrPub = this.localParticipant.getTrackPublication(TrackSources.ScreenShare);
1677
+ if (
1678
+ scrPub?.track?.mediaStreamTrack &&
1679
+ !scrPub.isMuted &&
1680
+ scrPub.track.mediaStreamTrack.readyState === "live"
1681
+ ) {
1682
+ return new MediaStream([scrPub.track.mediaStreamTrack]);
1683
+ }
1684
+ return null;
1685
+ }
1686
+
1687
+ patchMediaConnectionNegotiator(call) {
1688
+ const neg = call?._negotiator;
1689
+ if (!neg || neg._lmPatched) return;
1690
+ neg._lmPatched = true;
1691
+
1692
+ let sdpChain = Promise.resolve();
1693
+ const origHandleSDP = neg.handleSDP.bind(neg);
1694
+ const origHandleCandidate = neg.handleCandidate.bind(neg);
1695
+
1696
+ neg.handleSDP = (type, sdp) => {
1697
+ sdpChain = sdpChain
1698
+ .then(() => origHandleSDP(type, sdp))
1699
+ .catch(() => {});
1700
+ return sdpChain;
1701
+ };
1702
+
1703
+ neg.handleCandidate = (candidate) => {
1704
+ sdpChain = sdpChain
1705
+ .then(async () => {
1706
+ const pc = call.peerConnection;
1707
+ if (pc && !pc.remoteDescription) {
1708
+ for (let i = 0; i < 20 && !pc.remoteDescription; i++) {
1709
+ await new Promise((r) => setTimeout(r, 25));
1710
+ }
1711
+ }
1712
+ if (pc && pc.signalingState !== "closed") {
1713
+ return origHandleCandidate(candidate);
1714
+ }
1715
+ })
1716
+ .catch(() => {});
1717
+ return sdpChain;
1718
+ };
1719
+ }
1720
+
1721
+ callAllPeersScreenShare() {
1722
+ const screenStream = this.getLocalScreenMediaStream();
1723
+ if (!screenStream) return;
1724
+
1725
+ const dialedIdentities = new Set();
1726
+ for (const p of this.remoteParticipants.values()) {
1727
+ if (p.permissions?.canSubscribe && p.identity && !dialedIdentities.has(p.identity)) {
1728
+ dialedIdentities.add(p.identity);
1729
+ this.startDirectScreenShareToPeer(p.identity);
1730
+ if (p.peerId) {
1731
+ const peerIdCopy = p.peerId;
1732
+ const idCopy = p.identity;
1733
+ setTimeout(() => {
1734
+ if (this.isDisconnected || !this.getLocalScreenMediaStream()) return;
1735
+ const pc = this.directScreenPcs.get(`${idCopy}:screen`);
1736
+ const connected =
1737
+ pc &&
1738
+ (pc.connectionState === "connected" ||
1739
+ pc.iceConnectionState === "connected" ||
1740
+ pc.iceConnectionState === "completed");
1741
+ if (!connected) {
1742
+ this.callPeerScreenShare(peerIdCopy, idCopy);
1743
+ }
1744
+ }, 3200);
1745
+ }
1746
+ }
1747
+ }
1748
+
1749
+ if (!this.isHost && dialedIdentities.size === 0) {
1750
+ this.startDirectScreenShareToPeer("host");
1751
+ }
1752
+ }
1753
+
1754
+ callPeerScreenShare(remotePeerId, remoteIdentity) {
1755
+ if (!this.peer || !remotePeerId) return;
1756
+ const screenStream = this.getLocalScreenMediaStream();
1757
+ if (!screenStream) return;
1758
+
1759
+ const scrKey = `${remoteIdentity}:screen`;
1760
+ const prevCall = this.activeMediaCalls.get(scrKey);
1761
+ if (prevCall) {
1762
+ try {
1763
+ prevCall.close();
1764
+ } catch {}
1765
+ this.activeMediaCalls.delete(scrKey);
1766
+ }
1767
+
1768
+ const scrCall = this.peer.call(remotePeerId, screenStream, {
1769
+ metadata: {
1770
+ identity: this.localParticipant.identity,
1771
+ name: this.localParticipant.name,
1772
+ streamKind: "screen",
1773
+ },
1774
+ });
1775
+ if (scrCall) {
1776
+ this.patchMediaConnectionNegotiator(scrCall);
1777
+ this.bindMediaCallEvents(scrCall, remoteIdentity, "screen");
1778
+ this.activeMediaCalls.set(scrKey, scrCall);
1779
+ }
1780
+ }
1781
+
1782
+ callPeerWithLocalTracks(remotePeerId, remoteIdentity, forceScreen = false) {
1783
+ if (!this.peer || !remotePeerId) return;
1784
+
1785
+ const userStream = this.getLocalUserMediaStream();
1786
+ if (userStream) {
1787
+ const callKey = `${remoteIdentity}:user`;
1788
+ const existingUserCall = this.activeMediaCalls.get(callKey);
1789
+ const pc = existingUserCall?.peerConnection;
1790
+ const isCallAlive = Boolean(
1791
+ existingUserCall?.open &&
1792
+ pc &&
1793
+ pc.connectionState !== "closed" &&
1794
+ pc.connectionState !== "failed" &&
1795
+ pc.iceConnectionState !== "closed" &&
1796
+ pc.iceConnectionState !== "failed"
1797
+ );
1798
+
1799
+ let replacedInPlace = false;
1800
+ if (isCallAlive && typeof pc.getSenders === "function") {
1801
+ const senders = pc.getSenders();
1802
+ const newAudio = userStream.getAudioTracks()[0] || null;
1803
+ const newVideo = userStream.getVideoTracks()[0] || null;
1804
+ const audioSender = senders.find((s) => s.track?.kind === "audio");
1805
+ const videoSender = senders.find((s) => s.track?.kind === "video");
1806
+
1807
+ const canReplaceAudio = !newAudio || Boolean(audioSender);
1808
+ const canReplaceVideo = !newVideo || Boolean(videoSender);
1809
+ if (canReplaceAudio && canReplaceVideo) {
1810
+ if (audioSender && newAudio) audioSender.replaceTrack(newAudio).catch(() => {});
1811
+ if (videoSender) videoSender.replaceTrack(newVideo).catch(() => {});
1812
+ replacedInPlace = true;
1813
+ }
1814
+ }
1815
+
1816
+ if (!replacedInPlace) {
1817
+ if (existingUserCall) {
1818
+ try {
1819
+ existingUserCall.close();
1820
+ } catch {}
1821
+ }
1822
+ const call = this.peer.call(remotePeerId, userStream, {
1823
+ metadata: {
1824
+ identity: this.localParticipant.identity,
1825
+ name: this.localParticipant.name,
1826
+ streamKind: "user",
1827
+ },
1828
+ });
1829
+ if (call) {
1830
+ this.patchMediaConnectionNegotiator(call);
1831
+ this.bindMediaCallEvents(call, remoteIdentity, "user");
1832
+ this.activeMediaCalls.set(callKey, call);
1833
+ }
1834
+ }
1835
+ }
1836
+
1837
+ if (forceScreen && this.getLocalScreenMediaStream()) {
1838
+ this.startDirectScreenShareToPeer(remoteIdentity);
1839
+ }
1840
+ }
1841
+
1842
+ closeOutgoingScreenCalls() {
1843
+ for (const [key, call] of this.activeMediaCalls.entries()) {
1844
+ if (key.endsWith(":screen")) {
1845
+ try {
1846
+ call.close();
1847
+ } catch {}
1848
+ this.activeMediaCalls.delete(key);
1849
+ }
1850
+ }
1851
+ for (const [key, pc] of this.directScreenPcs.entries()) {
1852
+ if (key.endsWith(":screen") || !key.includes(":")) {
1853
+ try {
1854
+ pc.close();
1855
+ } catch {}
1856
+ this.directScreenPcs.delete(key);
1857
+ }
1858
+ }
1859
+ }
1860
+
1861
+ handleIncomingMediaCall(call) {
1862
+ const meta = call.metadata || {};
1863
+ const remoteIdentity = meta.identity || "";
1864
+ const streamKind = meta.streamKind || "user";
1865
+
1866
+ this.patchMediaConnectionNegotiator(call);
1867
+ this.bindMediaCallEvents(call, remoteIdentity, streamKind);
1868
+
1869
+ const answerStream = streamKind === "user" ? this.getLocalUserMediaStream() : undefined;
1870
+ try {
1871
+ call.answer(answerStream || undefined);
1872
+ } catch {}
1873
+
1874
+ if (call.remoteStream) {
1875
+ this.processIncomingRemoteStream(call.remoteStream, remoteIdentity, streamKind, meta.name);
1876
+ }
1877
+ }
1878
+
1879
+ processIncomingRemoteStream(remoteStream, remoteIdentity, streamKind, fallbackName) {
1880
+ if (!remoteIdentity || !remoteStream) return;
1881
+ let participant = this.remoteParticipants.get(remoteIdentity);
1882
+ if (!participant) {
1883
+ participant = new HybridParticipant({
1884
+ identity: remoteIdentity,
1885
+ name: fallbackName || "Participante",
1886
+ metadata: JSON.stringify({ role: "guest", status: "approved" }),
1887
+ permissions: { canPublish: true, canSubscribe: true, canPublishData: true },
1888
+ isLocal: false,
1889
+ room: this,
1890
+ });
1891
+ this.remoteParticipants.set(remoteIdentity, participant);
1892
+ }
1893
+
1894
+ if (streamKind === "screen") {
1895
+ const vTrack = remoteStream.getVideoTracks()[0];
1896
+ if (vTrack) {
1897
+ this.attachRemoteTrackToParticipant(
1898
+ participant,
1899
+ vTrack,
1900
+ TrackKinds.Video,
1901
+ TrackSources.ScreenShare
1902
+ );
1903
+ }
1904
+ return;
1905
+ }
1906
+
1907
+ for (const aTrack of remoteStream.getAudioTracks()) {
1908
+ this.attachRemoteTrackToParticipant(
1909
+ participant,
1910
+ aTrack,
1911
+ TrackKinds.Audio,
1912
+ TrackSources.Microphone
1913
+ );
1914
+ }
1915
+ for (const vTrack of remoteStream.getVideoTracks()) {
1916
+ this.attachRemoteTrackToParticipant(
1917
+ participant,
1918
+ vTrack,
1919
+ TrackKinds.Video,
1920
+ TrackSources.Camera
1921
+ );
1922
+ }
1923
+ }
1924
+
1925
+ bindMediaCallEvents(call, remoteIdentity, streamKind) {
1926
+ const fallbackName = call.metadata?.name || "Participante";
1927
+ call.on("stream", (remoteStream) => {
1928
+ this.processIncomingRemoteStream(remoteStream, remoteIdentity, streamKind, fallbackName);
1929
+ });
1930
+ }
1931
+
1932
+ attachRemoteTrackToParticipant(participant, mediaStreamTrack, kind, source) {
1933
+ if (!participant || !mediaStreamTrack) return;
1934
+
1935
+ if (!mediaStreamTrack._lmUnmuteBound) {
1936
+ mediaStreamTrack._lmUnmuteBound = true;
1937
+ mediaStreamTrack.addEventListener("unmute", () => {
1938
+ const curPub = participant.getTrackPublication(source);
1939
+ const curTrack = curPub?.track?.mediaStreamTrack;
1940
+ if (!curTrack || curTrack.readyState === "ended" || curTrack.muted) {
1941
+ if (curTrack?.id !== mediaStreamTrack.id) {
1942
+ this.attachRemoteTrackToParticipant(participant, mediaStreamTrack, kind, source);
1943
+ return;
1944
+ }
1945
+ }
1946
+ if (curPub && curTrack?.id === mediaStreamTrack.id) {
1947
+ curPub.isMuted = false;
1948
+ for (const el of curPub.track.attachedElements) {
1949
+ el.play?.().catch(() => {});
1950
+ }
1951
+ this.emit(RoomEvents.TrackUnmuted, curPub, participant);
1952
+ }
1953
+ });
1954
+ }
1955
+
1956
+ const existingPub = participant.getTrackPublication(source);
1957
+ if (existingPub?.track?.mediaStreamTrack?.id === mediaStreamTrack.id) {
1958
+ return;
1959
+ }
1960
+
1961
+ if (
1962
+ existingPub?.track?.mediaStreamTrack &&
1963
+ existingPub.track.mediaStreamTrack.readyState === "live" &&
1964
+ mediaStreamTrack.muted
1965
+ ) {
1966
+ return;
1967
+ }
1968
+
1969
+ if (existingPub?.track) {
1970
+ existingPub.track.detach().forEach((el) => el.remove());
1971
+ }
1972
+
1973
+ const hTrack = new HybridTrack(mediaStreamTrack, kind, source);
1974
+ const pub = new HybridPublication({
1975
+ trackSid: `${source}_${participant.identity}`,
1976
+ source,
1977
+ track: hTrack,
1978
+ isMuted: false,
1979
+ });
1980
+ participant.setPublication(source, pub);
1981
+
1982
+ this.emit(RoomEvents.TrackSubscribed, hTrack, pub, participant);
1983
+ }
1984
+
1985
+ // ============================================================================
1986
+ // TRANSPORTE CLOUDFLARE CALLS SFU
1987
+ // ============================================================================
1988
+ async initCloudflareCallsSession() {
1989
+ if (this.cfSessionId && this.cfPc) return;
1990
+ if (this.cfInitPromise) return this.cfInitPromise;
1991
+
1992
+ this.cfInitPromise = (async () => {
1993
+ const res = await this.apiPost("/api/cf-calls/session", {
1994
+ room_hash: this.roomHash,
1995
+ });
1996
+ if (!res?.sessionId) {
1997
+ throw new Error("No se recibió sessionId de Cloudflare Calls.");
1998
+ }
1999
+ this.cfSessionId = res.sessionId;
2000
+ this.localParticipant.cfSessionId = res.sessionId;
2001
+
2002
+ this.cfPc = new RTCPeerConnection({
2003
+ iceServers: this.iceServers,
2004
+ bundlePolicy: "max-bundle",
2005
+ });
2006
+
2007
+ this.cfPc.ontrack = (event) => {
2008
+ const mid = event.transceiver?.mid;
2009
+ const meta =
2010
+ mid !== null && mid !== undefined ? this.cfMidToRemoteMeta.get(String(mid)) : null;
2011
+ if (!meta) return;
2012
+ this.attachCloudflareRemoteTrack(meta, event.track);
2013
+ };
2014
+ })();
2015
+
2016
+ try {
2017
+ await this.cfInitPromise;
2018
+ } catch (err) {
2019
+ this.cfInitPromise = null;
2020
+ throw err;
2021
+ }
2022
+ }
2023
+
2024
+ attachCloudflareRemoteTrack(meta, mediaStreamTrack) {
2025
+ if (!meta || !mediaStreamTrack) return;
2026
+ const participant = this.remoteParticipants.get(meta.identity);
2027
+ if (!participant) return;
2028
+
2029
+ if (meta.trackName === "mic") {
2030
+ this.attachRemoteTrackToParticipant(
2031
+ participant,
2032
+ mediaStreamTrack,
2033
+ TrackKinds.Audio,
2034
+ TrackSources.Microphone
2035
+ );
2036
+ } else if (meta.trackName === "cam") {
2037
+ this.attachRemoteTrackToParticipant(
2038
+ participant,
2039
+ mediaStreamTrack,
2040
+ TrackKinds.Video,
2041
+ TrackSources.Camera
2042
+ );
2043
+ } else if (meta.trackName === "screen" || meta.trackName.startsWith("screen_")) {
2044
+ this.attachRemoteTrackToParticipant(
2045
+ participant,
2046
+ mediaStreamTrack,
2047
+ TrackKinds.Video,
2048
+ TrackSources.ScreenShare
2049
+ );
2050
+ }
2051
+ }
2052
+
2053
+ enqueueCfNegotiation(taskFn) {
2054
+ this.cfNegotiationChain = this.cfNegotiationChain.then(taskFn, taskFn);
2055
+ return this.cfNegotiationChain;
2056
+ }
2057
+
2058
+ async pushLocalTracksToCloudflare() {
2059
+ await this.initCloudflareCallsSession();
2060
+ return this.enqueueCfNegotiation(async () => {
2061
+ if (!this.cfPc || !this.cfSessionId) return;
2062
+
2063
+ const screenKey = this.currentScreenTrackName || "screen";
2064
+ const sourceMap = [
2065
+ { key: "mic", source: TrackSources.Microphone },
2066
+ { key: "cam", source: TrackSources.Camera },
2067
+ { key: screenKey, source: TrackSources.ScreenShare },
2068
+ ];
2069
+
2070
+ const newTracksToPush = [];
2071
+ const activeNames = [];
2072
+
2073
+ for (const { key, source } of sourceMap) {
2074
+ const pub = this.localParticipant.getTrackPublication(source);
2075
+ const mediaTrack = pub?.track?.mediaStreamTrack;
2076
+ const isLive = Boolean(mediaTrack && !pub.isMuted && mediaTrack.readyState === "live");
2077
+
2078
+ if (isLive) {
2079
+ activeNames.push(key);
2080
+ const existingTransceiver = this.cfTransceivers.get(key);
2081
+ if (existingTransceiver) {
2082
+ await existingTransceiver.sender.replaceTrack(mediaTrack);
2083
+ } else {
2084
+ const tr = this.cfPc.addTransceiver(mediaTrack, { direction: "sendonly" });
2085
+ this.cfTransceivers.set(key, tr);
2086
+ newTracksToPush.push({ key, tr });
2087
+ }
2088
+ }
2089
+ }
2090
+
2091
+ if (newTracksToPush.length > 0) {
2092
+ const offer = await this.cfPc.createOffer();
2093
+ await this.cfPc.setLocalDescription(offer);
2094
+
2095
+ const tracksPayload = newTracksToPush.map(({ key, tr }) => ({
2096
+ location: "local",
2097
+ mid: tr.mid,
2098
+ trackName: key,
2099
+ }));
2100
+
2101
+ const data = await this.apiPost("/api/cf-calls/tracks-new", {
2102
+ room_hash: this.roomHash,
2103
+ sessionId: this.cfSessionId,
2104
+ sessionDescription: {
2105
+ sdp: offer.sdp,
2106
+ type: "offer",
2107
+ },
2108
+ tracks: tracksPayload,
2109
+ });
2110
+
2111
+ if (data?.sessionDescription) {
2112
+ await this.cfPc.setRemoteDescription(
2113
+ new RTCSessionDescription(data.sessionDescription)
2114
+ );
2115
+ }
2116
+ }
2117
+
2118
+ this.localParticipant.cfPublishedTracks = activeNames;
2119
+ });
2120
+ }
2121
+
2122
+ async pullRemoteTracksFromCloudflare(remoteParticipant) {
2123
+ if (!this.cfCallsReady || !remoteParticipant?.cfSessionId) return;
2124
+ const remoteSessionId = remoteParticipant.cfSessionId;
2125
+ const published = Array.isArray(remoteParticipant.cfPublishedTracks)
2126
+ ? remoteParticipant.cfPublishedTracks
2127
+ : [];
2128
+
2129
+ const tracksToPull = [];
2130
+ for (const trackName of published) {
2131
+ const pullKey = `${remoteSessionId}:${trackName}`;
2132
+ if (!this.cfPulledRemoteKeys.has(pullKey)) {
2133
+ this.cfPulledRemoteKeys.add(pullKey);
2134
+ tracksToPull.push({
2135
+ location: "remote",
2136
+ sessionId: remoteSessionId,
2137
+ trackName,
2138
+ });
2139
+ }
2140
+ }
2141
+
2142
+ if (tracksToPull.length === 0) return;
2143
+
2144
+ await this.initCloudflareCallsSession();
2145
+ return this.enqueueCfNegotiation(async () => {
2146
+ if (!this.cfPc || !this.cfSessionId) return;
2147
+ try {
2148
+ const res = await this.apiPost("/api/cf-calls/tracks-new", {
2149
+ room_hash: this.roomHash,
2150
+ sessionId: this.cfSessionId,
2151
+ tracks: tracksToPull,
2152
+ });
2153
+
2154
+ let needsRetry = false;
2155
+ if (Array.isArray(res?.tracks)) {
2156
+ for (let i = 0; i < res.tracks.length; i++) {
2157
+ const t = res.tracks[i];
2158
+ const tName = t?.trackName || tracksToPull[i]?.trackName;
2159
+ if (!tName) continue;
2160
+ const pullKey = `${remoteSessionId}:${tName}`;
2161
+
2162
+ if (t && t.mid !== undefined && !t.errorCode) {
2163
+ this.cfMidToRemoteMeta.set(String(t.mid), {
2164
+ identity: remoteParticipant.identity,
2165
+ trackName: tName,
2166
+ });
2167
+ this.cfPullRetryCounts.delete(pullKey);
2168
+ } else {
2169
+ this.cfPulledRemoteKeys.delete(pullKey);
2170
+ const retries = (this.cfPullRetryCounts.get(pullKey) || 0) + 1;
2171
+ if (retries <= 8) {
2172
+ this.cfPullRetryCounts.set(pullKey, retries);
2173
+ needsRetry = true;
2174
+ }
2175
+ }
2176
+ }
2177
+ }
2178
+
2179
+ if (res?.requiresImmediateRenegotiation && res?.sessionDescription) {
2180
+ await this.cfPc.setRemoteDescription(
2181
+ new RTCSessionDescription(res.sessionDescription)
2182
+ );
2183
+ const answer = await this.cfPc.createAnswer();
2184
+ await this.cfPc.setLocalDescription(answer);
2185
+
2186
+ await this.apiPost("/api/cf-calls/renegotiate", {
2187
+ room_hash: this.roomHash,
2188
+ sessionId: this.cfSessionId,
2189
+ sessionDescription: {
2190
+ sdp: answer.sdp,
2191
+ type: "answer",
2192
+ },
2193
+ });
2194
+ }
2195
+
2196
+ for (const tr of this.cfPc.getTransceivers()) {
2197
+ if (tr.mid !== null && tr.mid !== undefined && tr.receiver?.track) {
2198
+ const meta = this.cfMidToRemoteMeta.get(String(tr.mid));
2199
+ if (meta) {
2200
+ this.attachCloudflareRemoteTrack(meta, tr.receiver.track);
2201
+ }
2202
+ }
2203
+ }
2204
+
2205
+ if (needsRetry && !this.isDisconnected) {
2206
+ setTimeout(() => {
2207
+ if (!this.isDisconnected) {
2208
+ this.pullRemoteTracksFromCloudflare(remoteParticipant);
2209
+ }
2210
+ }, 700);
2211
+ }
2212
+ } catch {
2213
+ for (const t of tracksToPull) {
2214
+ this.cfPulledRemoteKeys.delete(`${t.sessionId}:${t.trackName}`);
2215
+ }
2216
+ }
2217
+ });
2218
+ }
2219
+
2220
+ // ============================================================================
2221
+ // ENVÍO DE DATOS CIFRADOS (E2EE)
2222
+ // ============================================================================
2223
+ async sendData(payload, options = {}) {
2224
+ let text = "";
2225
+ if (typeof payload === "string") {
2226
+ text = payload;
2227
+ } else if (payload instanceof Uint8Array || payload instanceof ArrayBuffer) {
2228
+ text = new TextDecoder().decode(payload);
2229
+ } else {
2230
+ text = JSON.stringify(payload);
2231
+ }
2232
+
2233
+ let cipher = null;
2234
+ try {
2235
+ cipher = await encryptDataPacket(this.roomHash, text);
2236
+ } catch {
2237
+ cipher = null;
2238
+ }
2239
+
2240
+ const packet = {
2241
+ type: "app_data",
2242
+ senderId: this.localParticipant.identity,
2243
+ senderName: this.localParticipant.name,
2244
+ ...(cipher ? { cipher } : { text }),
2245
+ };
2246
+
2247
+ if (this.isHost) {
2248
+ for (const p of this.remoteParticipants.values()) {
2249
+ if (p.permissions?.canSubscribe) {
2250
+ this.enqueueHttpSignal(p.identity, packet);
2251
+ }
2252
+ }
2253
+ for (const [id, conn] of this.guestConns.entries()) {
2254
+ const gp = this.remoteParticipants.get(id);
2255
+ if (conn.open && gp?.permissions?.canSubscribe) {
2256
+ conn.send(packet);
2257
+ }
2258
+ }
2259
+ } else {
2260
+ this.enqueueHttpSignal("host", packet);
2261
+ if (this.hostConn?.open) {
2262
+ this.hostConn.send(packet);
2263
+ }
2264
+ }
2265
+ }
2266
+
2267
+ async kickParticipant(identity) {
2268
+ if (!this.isHost) return;
2269
+ try {
2270
+ await this.apiPost("/api/kick", {
2271
+ room_hash: this.roomHash,
2272
+ host_secret: this.hostSecret,
2273
+ identity,
2274
+ });
2275
+ } catch {}
2276
+
2277
+ this.enqueueHttpSignal(identity, { type: "kicked" });
2278
+ const conn = this.guestConns.get(identity);
2279
+ const p = this.remoteParticipants.get(identity);
2280
+ if (conn?.open) {
2281
+ conn.send({ type: "kicked" });
2282
+ setTimeout(() => conn.close(), 150);
2283
+ }
2284
+ this.guestConns.delete(identity);
2285
+ if (p) {
2286
+ this.cleanupRemoteParticipantTracks(p);
2287
+ this.remoteParticipants.delete(identity);
2288
+ this.emit(RoomEvents.ParticipantDisconnected, p);
2289
+ this.broadcastRoster();
2290
+ }
2291
+ }
2292
+
2293
+ async muteParticipant(identity, trackSid = "") {
2294
+ if (!this.isHost) return;
2295
+ try {
2296
+ if (trackSid) {
2297
+ await this.apiPost("/api/mute", {
2298
+ room_hash: this.roomHash,
2299
+ host_secret: this.hostSecret,
2300
+ identity,
2301
+ track_sid: trackSid,
2302
+ });
2303
+ }
2304
+ } catch {}
2305
+
2306
+ const payload = JSON.stringify({
2307
+ type: "remote_mute",
2308
+ targetIdentity: identity,
2309
+ senderName: this.localParticipant.name,
2310
+ });
2311
+ await this.sendData(payload);
2312
+ }
2313
+
2314
+ async endRoom() {
2315
+ if (!this.isHost) return;
2316
+ try {
2317
+ this.enqueueHttpSignal("all", { type: "room_ended" });
2318
+ for (const conn of this.guestConns.values()) {
2319
+ if (conn.open) {
2320
+ conn.send({ type: "room_ended" });
2321
+ }
2322
+ }
2323
+ await this.apiPost("/api/end-room", {
2324
+ room_hash: this.roomHash,
2325
+ host_secret: this.hostSecret,
2326
+ });
2327
+ } catch {}
2328
+ this.disconnect();
2329
+ }
2330
+
2331
+ cleanupRemoteParticipantTracks(participant) {
2332
+ for (const pub of participant.trackPublications.values()) {
2333
+ if (pub.track) {
2334
+ pub.track.detach().forEach((el) => el.remove());
2335
+ this.emit(RoomEvents.TrackUnsubscribed, pub.track, pub, participant);
2336
+ }
2337
+ }
2338
+ participant.trackPublications.clear();
2339
+ participant.videoTrackPublications.clear();
2340
+ }
2341
+
2342
+ disconnect() {
2343
+ if (this.isDisconnected) return;
2344
+ this.isDisconnected = true;
2345
+
2346
+ if (this.syncTimer) {
2347
+ clearInterval(this.syncTimer);
2348
+ this.syncTimer = null;
2349
+ }
2350
+ if (this.peerKeepAliveTimer) {
2351
+ clearInterval(this.peerKeepAliveTimer);
2352
+ this.peerKeepAliveTimer = null;
2353
+ }
2354
+
2355
+ for (const pub of this.localParticipant.trackPublications.values()) {
2356
+ pub.track?.stop();
2357
+ pub.track?.detach().forEach((el) => el.remove());
2358
+ }
2359
+ this.localParticipant.trackPublications.clear();
2360
+ this.localParticipant.videoTrackPublications.clear();
2361
+
2362
+ for (const p of this.remoteParticipants.values()) {
2363
+ this.cleanupRemoteParticipantTracks(p);
2364
+ }
2365
+ this.remoteParticipants.clear();
2366
+
2367
+ for (const call of this.activeMediaCalls.values()) {
2368
+ try {
2369
+ call.close();
2370
+ } catch {}
2371
+ }
2372
+ this.activeMediaCalls.clear();
2373
+
2374
+ for (const pc of this.directScreenPcs.values()) {
2375
+ try {
2376
+ pc.close();
2377
+ } catch {}
2378
+ }
2379
+ this.directScreenPcs.clear();
2380
+
2381
+ for (const pc of this.incomingScreenPcs.values()) {
2382
+ try {
2383
+ pc.close();
2384
+ } catch {}
2385
+ }
2386
+ this.incomingScreenPcs.clear();
2387
+ this.pendingScreenIce.clear();
2388
+
2389
+ try {
2390
+ this.cfPc?.close();
2391
+ } catch {}
2392
+ this.cfPc = null;
2393
+
2394
+ try {
2395
+ this.hostConn?.close();
2396
+ } catch {}
2397
+ for (const conn of this.guestConns.values()) {
2398
+ try {
2399
+ conn.close();
2400
+ } catch {}
2401
+ }
2402
+ this.guestConns.clear();
2403
+
2404
+ try {
2405
+ this.peer?.destroy();
2406
+ } catch {}
2407
+ this.peer = null;
2408
+
2409
+ this.emit(RoomEvents.Disconnected);
2410
+ }
2411
+ }
2412
+
2413
+
2414
+
2415
+
2416
+
2417
+ /**
2418
+ * Cliente principal estilo Axios para inicializar llamadas con 1 sola línea de código.
2419
+ */
2420
+ const LocalMeet = {
2421
+ RoomEvents,
2422
+ TrackSources,
2423
+ TrackKinds,
2424
+ EngineModes,
2425
+ LocalMeetRoom,
2426
+ HybridTrack,
2427
+ HybridPublication,
2428
+ HybridParticipant,
2429
+
2430
+ /**
2431
+ * Crea una sala nueva como anfitrión en el backend y devuelve una instancia lista de LocalMeetRoom.
2432
+ *
2433
+ * @param {Object} options
2434
+ * @param {string} options.serverUrl - URL del Worker o servidor de señalización (ej: "https://mi-meet.workers.dev")
2435
+ * @param {string} [options.hostName="Anfitrión"] - Nombre a mostrar del anfitrión
2436
+ * @param {string} [options.title=""] - Título de la reunión
2437
+ * @param {string} [options.engine="cf-calls"] - "cf-calls" | "p2p" | "livekit"
2438
+ * @param {string} [options.accessKey=""] - Clave HOST_ACCESS_KEY si el servidor la requiere
2439
+ * @param {string} [options.customHash=""] - Hash de sala a reutilizar si ya eres el anfitrión
2440
+ * @param {string} [options.hostSecret=""] - Secreto de anfitrión previo
2441
+ * @returns {Promise<LocalMeetRoom>}
2442
+ */
2443
+ async createRoom(options = {}) {
2444
+ const {
2445
+ serverUrl = typeof window !== "undefined" ? window.location.origin : "",
2446
+ hostName = "Anfitrión",
2447
+ title = "",
2448
+ engine = EngineModes.CloudflareCalls,
2449
+ accessKey = "",
2450
+ customHash = "",
2451
+ hostSecret = "",
2452
+ debug = false,
2453
+ } = options;
2454
+
2455
+ const cleanServer = (serverUrl || "").replace(/\/+$/, "");
2456
+ const res = await fetch(`${cleanServer}/api/host-join`, {
2457
+ method: "POST",
2458
+ headers: { "Content-Type": "application/json" },
2459
+ body: JSON.stringify({
2460
+ host_name: hostName,
2461
+ title,
2462
+ engine,
2463
+ access_key: accessKey,
2464
+ room_hash: customHash,
2465
+ host_secret: hostSecret,
2466
+ }),
2467
+ });
2468
+
2469
+ const data = await res.json();
2470
+ if (!res.ok) {
2471
+ throw new Error(data?.detail || `Error al crear sala (${res.status})`);
2472
+ }
2473
+
2474
+ const room = new LocalMeetRoom({
2475
+ serverUrl: cleanServer,
2476
+ roomHash: data.room_hash,
2477
+ title: data.title || title,
2478
+ identity: data.identity,
2479
+ displayName: data.host_name || hostName,
2480
+ isHost: true,
2481
+ hostSecret: data.host_secret,
2482
+ engine: data.engine || engine,
2483
+ cfCallsReady: Boolean(data.cf_calls_ready),
2484
+ iceServers: data.ice_servers,
2485
+ startInWaitingRoom: false,
2486
+ debug,
2487
+ });
2488
+
2489
+ return room;
2490
+ },
2491
+
2492
+ /**
2493
+ * Se une a una sala existente como invitado.
2494
+ *
2495
+ * @param {Object} options
2496
+ * @param {string} options.serverUrl - URL del Worker o servidor
2497
+ * @param {string} options.roomHash - Hash de la sala (ej: "c4f2-9k81-m3n2")
2498
+ * @param {string} [options.guestName="Invitado"] - Nombre del participante
2499
+ * @returns {Promise<LocalMeetRoom>}
2500
+ */
2501
+ async join(options = {}) {
2502
+ const {
2503
+ serverUrl = typeof window !== "undefined" ? window.location.origin : "",
2504
+ roomHash = "",
2505
+ guestName = "Invitado",
2506
+ debug = false,
2507
+ } = options;
2508
+
2509
+ const cleanServer = (serverUrl || "").replace(/\/+$/, "");
2510
+ const cleanHash = (roomHash || "").toLowerCase().trim();
2511
+
2512
+ const res = await fetch(`${cleanServer}/api/guest-join`, {
2513
+ method: "POST",
2514
+ headers: { "Content-Type": "application/json" },
2515
+ body: JSON.stringify({
2516
+ room_hash: cleanHash,
2517
+ guest_name: guestName,
2518
+ }),
2519
+ });
2520
+
2521
+ const data = await res.json();
2522
+ if (!res.ok) {
2523
+ throw new Error(data?.detail || `Error al unirse a sala (${res.status})`);
2524
+ }
2525
+
2526
+ const room = new LocalMeetRoom({
2527
+ serverUrl: cleanServer,
2528
+ roomHash: data.room_hash,
2529
+ title: `Sala ${data.room_hash}`,
2530
+ identity: data.identity,
2531
+ displayName: data.guest_name || guestName,
2532
+ isHost: false,
2533
+ engine: data.engine || EngineModes.CloudflareCalls,
2534
+ cfCallsReady: Boolean(data.cf_calls_ready),
2535
+ iceServers: data.ice_servers,
2536
+ startInWaitingRoom: true,
2537
+ debug,
2538
+ });
2539
+
2540
+ return room;
2541
+ },
2542
+ };
2543
+
2544
+
2545
+
2546
+ module.exports = LocalMeet;
2547
+ module.exports.default = LocalMeet;
2548
+ module.exports.LocalMeet = LocalMeet;
2549
+ module.exports.LocalMeetRoom = LocalMeetRoom;
2550
+ module.exports.HybridTrack = HybridTrack;
2551
+ module.exports.HybridPublication = HybridPublication;
2552
+ module.exports.HybridParticipant = HybridParticipant;
2553
+ module.exports.RoomEvents = RoomEvents;
2554
+ module.exports.TrackSources = TrackSources;
2555
+ module.exports.TrackKinds = TrackKinds;
2556
+ module.exports.EngineModes = EngineModes;
2557
+ module.exports.encryptDataPacket = encryptDataPacket;
2558
+ module.exports.decryptDataPacket = decryptDataPacket;