@afosecure/meshsdk 0.1.1 → 0.1.2

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.mjs ADDED
@@ -0,0 +1,1404 @@
1
+ // src/react/useLocalParticipant.tsx
2
+ import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
3
+
4
+ // src/react/MeetingProvider.tsx
5
+ import { createContext, useContext, useMemo, useRef } from "react";
6
+
7
+ // src/config/ws.ts
8
+ var SDK_CONFIG = {
9
+ wsUrl: "wss://https://rust-video-server-sfyf.onrender.com/ws",
10
+ baseUrl: "https://https://rust-video-server-sfyf.onrender.com"
11
+ };
12
+
13
+ // src/core/MeetingState.ts
14
+ var MeetingState = class {
15
+ constructor() {
16
+ this.participants = /* @__PURE__ */ new Map();
17
+ this.localParticipant = null;
18
+ this.localStream = null;
19
+ this.chatMessages = /* @__PURE__ */ new Map();
20
+ this.presenterId = null;
21
+ this.listeners = /* @__PURE__ */ new Map();
22
+ }
23
+ // ---- reactive system ----
24
+ subscribe(scope, fn) {
25
+ if (!this.listeners.has(scope)) {
26
+ this.listeners.set(scope, /* @__PURE__ */ new Set());
27
+ }
28
+ this.listeners.get(scope).add(fn);
29
+ return () => {
30
+ this.listeners.get(scope)?.delete(fn);
31
+ };
32
+ }
33
+ notify(scope) {
34
+ this.listeners.get(scope)?.forEach((fn) => fn());
35
+ }
36
+ setPresenterId(id) {
37
+ if (this.presenterId === id) return;
38
+ this.presenterId = id;
39
+ this.notify("presenter");
40
+ this.notify("participants");
41
+ }
42
+ // ---- participants ----
43
+ addParticipant(p) {
44
+ if (this.participants.has(p.id)) return false;
45
+ const next = new Map(this.participants);
46
+ next.set(p.id, p);
47
+ this.participants = next;
48
+ this.notify("participants");
49
+ return true;
50
+ }
51
+ removeParticipant(id) {
52
+ const next = new Map(this.participants);
53
+ next.delete(id);
54
+ this.participants = next;
55
+ this.notify("participants");
56
+ }
57
+ updateParticipantMedia(id, patch) {
58
+ const p = this.participants.get(id);
59
+ if (!p) return;
60
+ const updated = {
61
+ ...p,
62
+ media: {
63
+ stream: null,
64
+ screenStream: void 0,
65
+ cameraTrack: void 0,
66
+ screenTrack: void 0,
67
+ audioTrack: void 0,
68
+ micEnabled: true,
69
+ camEnabled: true,
70
+ isScreenSharing: false,
71
+ ...p.media,
72
+ ...patch
73
+ }
74
+ };
75
+ const next = new Map(this.participants);
76
+ next.set(id, updated);
77
+ this.participants = next;
78
+ this.notify(`participant:${id}`);
79
+ this.notify("participants");
80
+ }
81
+ updateLocalParticipant(patch) {
82
+ const prev = this.localParticipant;
83
+ if (!prev) {
84
+ this.localParticipant = {
85
+ id: patch.id ?? "",
86
+ name: patch.name ?? "",
87
+ media: {
88
+ stream: patch.media?.stream ?? null,
89
+ // ◄ FIX: Capture the stream from the patch here
90
+ screenStream: patch.media?.screenStream,
91
+ cameraTrack: patch.media?.cameraTrack,
92
+ screenTrack: patch.media?.screenTrack,
93
+ audioTrack: patch.media?.audioTrack,
94
+ micEnabled: patch.media?.micEnabled ?? true,
95
+ camEnabled: patch.media?.camEnabled ?? true,
96
+ isScreenSharing: patch.media?.isScreenSharing ?? false
97
+ }
98
+ };
99
+ this.notify("localParticipant");
100
+ return;
101
+ }
102
+ const prevMedia = prev.media ?? {
103
+ stream: null,
104
+ screenStream: void 0,
105
+ cameraTrack: void 0,
106
+ screenTrack: void 0,
107
+ audioTrack: void 0,
108
+ micEnabled: true,
109
+ camEnabled: true,
110
+ isScreenSharing: false
111
+ };
112
+ const nextMedia = {
113
+ stream: patch.media?.stream ?? prevMedia.stream,
114
+ screenStream: patch.media?.screenStream ?? prevMedia.screenStream,
115
+ cameraTrack: patch.media?.cameraTrack ?? prevMedia.cameraTrack,
116
+ screenTrack: patch.media?.screenTrack ?? prevMedia.screenTrack,
117
+ audioTrack: patch.media?.audioTrack ?? prevMedia.audioTrack,
118
+ micEnabled: patch.media?.micEnabled ?? prevMedia.micEnabled,
119
+ camEnabled: patch.media?.camEnabled ?? prevMedia.camEnabled,
120
+ isScreenSharing: patch.media?.isScreenSharing ?? prevMedia.isScreenSharing
121
+ };
122
+ this.localParticipant = {
123
+ ...prev,
124
+ id: patch.id ?? prev.id,
125
+ name: patch.name ?? prev.name,
126
+ media: nextMedia
127
+ };
128
+ this.notify("localParticipant");
129
+ }
130
+ // ---- chat ----
131
+ addChatMessage(msg) {
132
+ this.chatMessages.set(msg.id, msg);
133
+ this.notify("chat");
134
+ }
135
+ getChatMessages() {
136
+ return Array.from(this.chatMessages.values()).sort(
137
+ (a, b) => a.timestamp - b.timestamp
138
+ );
139
+ }
140
+ clearChat() {
141
+ this.chatMessages.clear();
142
+ this.notify("chat");
143
+ }
144
+ // ---- helpers ----
145
+ getParticipants() {
146
+ return Array.from(this.participants.values());
147
+ }
148
+ getParticipant(id) {
149
+ return this.participants.get(id) ?? null;
150
+ }
151
+ resetRemoteState() {
152
+ this.participants.clear();
153
+ this.chatMessages.clear();
154
+ this.presenterId = null;
155
+ this.notify("participants");
156
+ this.notify("chat");
157
+ this.notify("presenter");
158
+ }
159
+ };
160
+
161
+ // src/core/VideoCore.ts
162
+ var VideoSDKCore = class {
163
+ constructor(events = {}, url = SDK_CONFIG.wsUrl) {
164
+ this.events = events;
165
+ this.url = url;
166
+ this.ws = null;
167
+ this.peers = {};
168
+ this.initiators = /* @__PURE__ */ new Set();
169
+ this.lastPong = Date.now();
170
+ this.iceServers = [];
171
+ this.intentionalDisconnect = false;
172
+ this.room = {
173
+ id: null,
174
+ name: null
175
+ };
176
+ this.localStream = null;
177
+ this.screenStream = null;
178
+ this.isScreenSharing = false;
179
+ this.screenSenders = {};
180
+ this.pingInterval = null;
181
+ this.pendingIceCandidates = {};
182
+ this.pendingOffers = {};
183
+ this.reconnectAttempts = 0;
184
+ this.participantName = "";
185
+ // Track if we're in the waiting room (pending approval)
186
+ this.isWaitingForApproval = false;
187
+ this.pendingRequestId = null;
188
+ this.iceTransportPolicy = "all";
189
+ this.state = new MeetingState();
190
+ this.events = events;
191
+ this.url = url;
192
+ this.myId = localStorage.getItem("vsdk_id") || crypto.randomUUID();
193
+ localStorage.setItem("vsdk_id", this.myId);
194
+ }
195
+ emitError(code, message, raw, recoverable = true) {
196
+ const err = {
197
+ code,
198
+ message,
199
+ raw,
200
+ roomId: this.room.id,
201
+ userId: this.myId,
202
+ recoverable
203
+ };
204
+ this.events.onError?.(err);
205
+ this.joinRejecter?.(err);
206
+ this.joinRejecter = void 0;
207
+ console.error("[MeetingSDK Error]", err);
208
+ }
209
+ // ---------------- STREAM ----------------
210
+ async initLocal(video, name) {
211
+ this.participantName = name;
212
+ try {
213
+ this.localStream = await navigator.mediaDevices.getUserMedia({
214
+ video: {
215
+ width: { ideal: 1280 },
216
+ height: { ideal: 720 }
217
+ },
218
+ audio: {
219
+ echoCancellation: true,
220
+ noiseSuppression: true,
221
+ autoGainControl: true
222
+ }
223
+ });
224
+ const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
225
+ const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
226
+ if (!hasVideo || !hasAudio) {
227
+ throw new Error(`Missing tracks: video=${hasVideo}, audio=${hasAudio}`);
228
+ }
229
+ video.srcObject = this.localStream;
230
+ this.state.updateLocalParticipant({
231
+ id: this.myId,
232
+ name: this.participantName,
233
+ media: {
234
+ stream: this.localStream,
235
+ micEnabled: true,
236
+ camEnabled: true,
237
+ isScreenSharing: false
238
+ }
239
+ });
240
+ this.state.localStream = this.localStream;
241
+ } catch (err) {
242
+ this.emitError("GET_USER_MEDIA_FAILED", err?.message, err, false);
243
+ throw err;
244
+ }
245
+ }
246
+ // ---------------- CONNECT ----------------
247
+ async connect(roomId, name) {
248
+ this.room.id = roomId;
249
+ this.reset();
250
+ return new Promise((resolve, reject) => {
251
+ this.joinResolver = resolve;
252
+ this.joinRejecter = reject;
253
+ this.ws = new WebSocket(this.url);
254
+ this.ws.onopen = () => {
255
+ console.log("WebSocket connected, sending JOIN...");
256
+ this.send({
257
+ type: "JOIN",
258
+ room_id: roomId,
259
+ user_id: this.myId,
260
+ sender_name: name,
261
+ camera_stream_id: this.localStream?.id.replace(/[{}]/g, ""),
262
+ audio_muted: this.state.localParticipant?.media?.micEnabled,
263
+ video_muted: this.state.localParticipant?.media?.camEnabled
264
+ });
265
+ };
266
+ this.ws.onerror = (err) => {
267
+ this.emitError("WS_ERROR", "WebSocket encountered an error", err, true);
268
+ };
269
+ this.ws.onclose = (e) => {
270
+ this.joinRejecter?.({
271
+ code: "WS_CLOSED",
272
+ message: "Connection closed before join completed",
273
+ raw: e
274
+ });
275
+ this.joinRejecter = void 0;
276
+ if (this.intentionalDisconnect) {
277
+ return;
278
+ }
279
+ if (e.code === 1e3 || e.code === 1001) {
280
+ return;
281
+ }
282
+ if (this.isWaitingForApproval) {
283
+ return;
284
+ }
285
+ this.scheduleReconnect();
286
+ };
287
+ this.ws.onmessage = async (e) => {
288
+ await this.handle(JSON.parse(e.data));
289
+ };
290
+ });
291
+ }
292
+ async joinMeeting(config) {
293
+ const { roomId, name, audioMuted = false, videoMuted = false } = config;
294
+ if (!roomId || !name) {
295
+ throw new Error("roomId and name are required to join meeting");
296
+ }
297
+ this.participantName = name;
298
+ if (!this.localStream) {
299
+ this.localStream = await navigator.mediaDevices.getUserMedia({
300
+ video: true,
301
+ audio: true
302
+ });
303
+ }
304
+ this.localStream.getAudioTracks().forEach((t) => {
305
+ t.enabled = !audioMuted;
306
+ });
307
+ this.localStream.getVideoTracks().forEach((t) => {
308
+ t.enabled = !videoMuted;
309
+ });
310
+ this.state.updateLocalParticipant({
311
+ id: this.myId,
312
+ name: this.participantName,
313
+ media: {
314
+ stream: this.localStream,
315
+ micEnabled: !audioMuted,
316
+ camEnabled: !videoMuted,
317
+ isScreenSharing: false
318
+ }
319
+ });
320
+ this.state.localStream = this.localStream;
321
+ await this.connect(roomId, name);
322
+ }
323
+ getMeeting() {
324
+ return this.room;
325
+ }
326
+ toggleMic() {
327
+ const mediaState = this.state.localParticipant?.media;
328
+ if (!mediaState) return;
329
+ const nextEnabled = !mediaState.micEnabled;
330
+ this.localStream?.getAudioTracks().forEach((t) => t.enabled = nextEnabled);
331
+ this.state.updateLocalParticipant({
332
+ id: this.myId,
333
+ name: this.participantName,
334
+ media: {
335
+ ...mediaState,
336
+ micEnabled: nextEnabled
337
+ }
338
+ });
339
+ this.send({
340
+ type: "MEDIA_STATE",
341
+ kind: "audio",
342
+ enabled: nextEnabled
343
+ });
344
+ }
345
+ toggleCam() {
346
+ const mediaState = this.state.localParticipant?.media;
347
+ if (!mediaState) return;
348
+ const nextEnabled = !mediaState.camEnabled;
349
+ this.localStream?.getVideoTracks().forEach((t) => t.enabled = nextEnabled);
350
+ this.state.updateLocalParticipant({
351
+ id: this.myId,
352
+ name: this.participantName,
353
+ media: {
354
+ ...mediaState,
355
+ camEnabled: nextEnabled
356
+ }
357
+ });
358
+ this.send({
359
+ type: "MEDIA_STATE",
360
+ kind: "video",
361
+ enabled: nextEnabled
362
+ });
363
+ }
364
+ scheduleReconnect() {
365
+ if (!this.room.id) return;
366
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
367
+ clearTimeout(this.reconnectTimer);
368
+ this.reconnectTimer = window.setTimeout(async () => {
369
+ try {
370
+ await this.connect(this.room.id, this.participantName);
371
+ this.reconnectAttempts = 0;
372
+ } catch {
373
+ this.reconnectAttempts++;
374
+ this.scheduleReconnect();
375
+ }
376
+ }, delay);
377
+ }
378
+ startHeartbeat() {
379
+ this.stopHeartbeat();
380
+ this.pingInterval = setInterval(() => {
381
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
382
+ this.send({
383
+ type: "PING",
384
+ client_ts: Date.now()
385
+ });
386
+ }, 2e4);
387
+ }
388
+ stopHeartbeat() {
389
+ if (this.pingInterval) {
390
+ clearInterval(this.pingInterval);
391
+ this.pingInterval = null;
392
+ }
393
+ }
394
+ // ---------------- RESET ----------------
395
+ reset() {
396
+ Object.values(this.peers).forEach((pc) => pc.close());
397
+ this.peers = {};
398
+ this.initiators.clear();
399
+ this.pendingIceCandidates = {};
400
+ this.state.resetRemoteState();
401
+ }
402
+ async handleJoinApproved(msg) {
403
+ console.log("JOIN_APPROVED received, sending new JOIN...");
404
+ this.events.onEntryResponded?.({
405
+ participantId: msg.user_id,
406
+ decision: "approved"
407
+ });
408
+ this.isWaitingForApproval = false;
409
+ this.pendingRequestId = null;
410
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
411
+ this.send({
412
+ type: "JOIN",
413
+ room_id: this.room.id,
414
+ user_id: this.myId,
415
+ sender_name: this.participantName
416
+ });
417
+ console.log("Sent new JOIN after approval");
418
+ }
419
+ }
420
+ async handle(msg) {
421
+ var _a, _b, _c, _d;
422
+ if (msg.sender === this.myId) return;
423
+ switch (msg.type) {
424
+ case "PONG":
425
+ this.lastPong = Date.now();
426
+ break;
427
+ case "OFFER":
428
+ console.log("[Offer] Received from", msg.sender, {
429
+ sdp: msg.payload.substring(0, 200)
430
+ });
431
+ await this.handleOffer(msg.payload, msg.sender);
432
+ break;
433
+ case "ANSWER": {
434
+ const pc = this.peers[msg.sender];
435
+ console.log("[Answer] Received from", msg.sender, {
436
+ signalingState: pc?.signalingState,
437
+ iceConnectionState: pc?.iceConnectionState,
438
+ connectionState: pc?.connectionState
439
+ });
440
+ if (!pc) return;
441
+ if (pc.signalingState !== "have-local-offer") {
442
+ console.warn(
443
+ `[Signaling] Unexpected ANSWER in state "${pc.signalingState}", ignoring`
444
+ );
445
+ return;
446
+ }
447
+ try {
448
+ await pc.setRemoteDescription({
449
+ type: "answer",
450
+ sdp: msg.payload
451
+ });
452
+ await this.flushIce(msg.sender, pc);
453
+ } catch (err) {
454
+ console.error("[Signaling] Failed to apply answer:", err);
455
+ this.emitError(
456
+ "ANSWER_FAILED",
457
+ `Failed to apply answer from ${msg.sender}`,
458
+ err,
459
+ true
460
+ );
461
+ }
462
+ break;
463
+ }
464
+ case "ICE": {
465
+ const candidate = JSON.parse(msg.payload);
466
+ let pc = this.peers[msg.sender];
467
+ if (!pc) {
468
+ (_a = this.pendingIceCandidates)[_b = msg.sender] ?? (_a[_b] = []);
469
+ this.pendingIceCandidates[msg.sender].push(candidate);
470
+ break;
471
+ }
472
+ if (!pc.remoteDescription) {
473
+ (_c = this.pendingIceCandidates)[_d = msg.sender] ?? (_c[_d] = []);
474
+ this.pendingIceCandidates[msg.sender].push(candidate);
475
+ break;
476
+ }
477
+ try {
478
+ await pc.addIceCandidate(candidate);
479
+ } catch (err) {
480
+ console.warn("ICE error:", err);
481
+ }
482
+ break;
483
+ }
484
+ case "EXISTING_USERS":
485
+ if (msg.presenterId) {
486
+ this.state.setPresenterId(msg.presenterId);
487
+ this.events.onScreenShareStarted?.(msg.presenterId, null);
488
+ this.state.setPresenterId(msg.presenterId);
489
+ }
490
+ for (const p of msg.participants || []) {
491
+ if (!p?.id || p.id === this.myId) continue;
492
+ const structuredParticipant = {
493
+ id: p.id,
494
+ name: p.name,
495
+ isHost: p.isHost,
496
+ isPresenter: p.isPresenter,
497
+ media: {
498
+ stream: null,
499
+ screenStream: void 0,
500
+ cameraTrack: void 0,
501
+ screenTrack: void 0,
502
+ audioTrack: void 0,
503
+ micEnabled: p.micEnabled ?? true,
504
+ camEnabled: p.camEnabled ?? true,
505
+ isScreenSharing: p.isScreenSharing ?? false,
506
+ remoteScreenStreamId: p.remoteScreenStreamId || void 0,
507
+ cameraStreamId: p.cameraId || void 0
508
+ }
509
+ };
510
+ this.state.addParticipant(structuredParticipant);
511
+ this.events.onUserJoined?.(structuredParticipant);
512
+ if (p.isScreenSharing && p.remoteScreenStreamId) {
513
+ this.state.setPresenterId(p.id);
514
+ this.state.updateParticipantMedia(p.id, {
515
+ isScreenSharing: true,
516
+ remoteScreenStreamId: p.remoteScreenStreamId,
517
+ cameraStreamId: p.cameraId || null
518
+ });
519
+ }
520
+ if (this.shouldInitiate(p.id)) {
521
+ await this.createOffer(p.id);
522
+ }
523
+ }
524
+ break;
525
+ case "JOINED": {
526
+ if (msg.iceServers) {
527
+ this.iceServers = msg.iceServers;
528
+ }
529
+ this.room.name = msg.room_name;
530
+ this.isWaitingForApproval = false;
531
+ this.pendingRequestId = null;
532
+ this.intentionalDisconnect = false;
533
+ this.reconnectAttempts = 0;
534
+ if (Object.keys(this.pendingOffers).length > 0) {
535
+ console.log(
536
+ "Processing",
537
+ Object.keys(this.pendingOffers).length,
538
+ "pending offers"
539
+ );
540
+ for (const [peerId2, sdp] of Object.entries(this.pendingOffers)) {
541
+ await this.handleOffer(sdp, peerId2);
542
+ }
543
+ this.pendingOffers = {};
544
+ }
545
+ const media = this.state.localParticipant?.media;
546
+ if (media) {
547
+ this.send({
548
+ type: "MEDIA_STATE",
549
+ kind: "audio",
550
+ enabled: !!media.micEnabled
551
+ });
552
+ this.send({
553
+ type: "MEDIA_STATE",
554
+ kind: "video",
555
+ enabled: !!media.camEnabled
556
+ });
557
+ }
558
+ this.startHeartbeat();
559
+ this.joinResolver?.();
560
+ this.joinResolver = void 0;
561
+ this.joinRejecter = void 0;
562
+ break;
563
+ }
564
+ case "USER_JOINED": {
565
+ const p = msg.participant;
566
+ if (!p?.id || p.id === this.myId) return;
567
+ this.state.addParticipant(p);
568
+ this.events.onUserJoined?.(p);
569
+ if (this.shouldInitiate(p.id)) {
570
+ await this.createOffer(p.id);
571
+ }
572
+ break;
573
+ }
574
+ case "JOIN_PENDING": {
575
+ const req = msg.request;
576
+ this.isWaitingForApproval = true;
577
+ this.pendingRequestId = req.request_id;
578
+ this.events.onEntryRequested?.({
579
+ requestId: req.request_id,
580
+ userId: req.user_id,
581
+ name: req.name
582
+ });
583
+ break;
584
+ }
585
+ case "JOIN_REQUEST": {
586
+ const req = msg.request;
587
+ this.events.onEntryRequested?.({
588
+ requestId: req.id,
589
+ userId: req.user_id,
590
+ name: req.name
591
+ });
592
+ break;
593
+ }
594
+ case "JOIN_APPROVED": {
595
+ await this.handleJoinApproved(msg);
596
+ break;
597
+ }
598
+ case "JOIN_REJECTED": {
599
+ const decision = "rejected";
600
+ this.isWaitingForApproval = false;
601
+ this.pendingRequestId = null;
602
+ this.events.onEntryResponded?.({
603
+ participantId: msg.user_id,
604
+ decision
605
+ });
606
+ break;
607
+ }
608
+ case "USER_LEFT":
609
+ const peerId = msg.participant.id;
610
+ this.closePeer(peerId);
611
+ this.state.removeParticipant(peerId);
612
+ this.events.onUserLeft?.(peerId);
613
+ break;
614
+ case "MEDIA_STATE_CHANGE": {
615
+ const peerId2 = msg.peerId;
616
+ const { kind, enabled } = msg;
617
+ if (kind === "audio") {
618
+ this.state.updateParticipantMedia(peerId2, { micEnabled: enabled });
619
+ this.events.onMicToggled?.(peerId2, enabled);
620
+ } else if (kind === "video") {
621
+ this.state.updateParticipantMedia(peerId2, { camEnabled: enabled });
622
+ this.events.onCamToggled?.(peerId2, enabled);
623
+ }
624
+ break;
625
+ }
626
+ case "CHAT_MESSAGE": {
627
+ const newMsg = msg.data;
628
+ if (newMsg.sender_id === this.myId) break;
629
+ this.state.addChatMessage({
630
+ id: newMsg.id,
631
+ text: newMsg.message,
632
+ sender_id: newMsg.sender_id,
633
+ sender_name: newMsg.sender_name,
634
+ timestamp: new Date(newMsg.timestamp).getTime(),
635
+ target: newMsg.target
636
+ });
637
+ this.events.onChatMessage?.(msg);
638
+ break;
639
+ }
640
+ case "SCREEN_SHARE_START": {
641
+ const peerId2 = msg.peerId;
642
+ this.state.updateParticipantMedia(peerId2, {
643
+ isScreenSharing: true,
644
+ remoteScreenStreamId: msg.stream_id,
645
+ cameraStreamId: msg?.camera_stream_id
646
+ });
647
+ if (!this.state.presenterId) {
648
+ this.state.setPresenterId(peerId2);
649
+ }
650
+ const screenStream = this.state.getParticipant(peerId2)?.media?.screenStream;
651
+ this.events.onScreenShareStarted?.(peerId2, screenStream || null);
652
+ break;
653
+ }
654
+ case "SCREEN_SHARE_STOP": {
655
+ const peerId2 = msg.peerId;
656
+ this.state.updateParticipantMedia(peerId2, { isScreenSharing: false });
657
+ if (this.state.presenterId === peerId2) {
658
+ this.state.setPresenterId(null);
659
+ }
660
+ this.events.onScreenShareStopped?.(peerId2);
661
+ break;
662
+ }
663
+ case "ERROR": {
664
+ const fatal = msg?.fatal === true;
665
+ this.emitError(
666
+ "WS_ERROR",
667
+ msg?.message || "Unknown error",
668
+ msg,
669
+ !fatal
670
+ );
671
+ if (fatal) {
672
+ this.disconnect();
673
+ }
674
+ return;
675
+ }
676
+ }
677
+ }
678
+ // ---------------- PEER ----------------
679
+ createPeer(id) {
680
+ if (!this.localStream) throw new Error("No local stream");
681
+ if (!this.iceServers || this.iceServers.length === 0) {
682
+ throw new Error(
683
+ "ICE Servers not configured. Backend must provide iceServers on JOIN."
684
+ );
685
+ }
686
+ const pc = new RTCPeerConnection({
687
+ iceServers: this.iceServers,
688
+ iceTransportPolicy: this.iceTransportPolicy
689
+ });
690
+ pc.ontrack = (event) => {
691
+ const incomingStream = event.streams?.[0] || new MediaStream([event.track]);
692
+ const streamId = incomingStream?.id?.replace(/[{}]/g, "");
693
+ const participant = this.state.getParticipant(id);
694
+ const isCameraStream = streamId && participant?.media?.cameraStreamId && streamId === participant?.media?.cameraStreamId;
695
+ const isScreenStream = streamId && participant?.media?.remoteScreenStreamId && streamId === participant?.media?.remoteScreenStreamId;
696
+ if (event.track.muted) {
697
+ event.track.onunmute = () => {
698
+ console.log(`${event.track.kind} track unmuted for ${id}`);
699
+ };
700
+ }
701
+ if (isScreenStream) {
702
+ const videoTrack = event.track.kind === "video" ? event.track : incomingStream.getVideoTracks()[0] || participant?.media?.screenTrack;
703
+ this.state.updateParticipantMedia(id, {
704
+ screenStream: incomingStream,
705
+ screenTrack: videoTrack,
706
+ remoteScreenStreamId: incomingStream.id,
707
+ isScreenSharing: true
708
+ });
709
+ if (!this.state.presenterId) {
710
+ this.state.setPresenterId(id);
711
+ }
712
+ this.events.onScreenShareStarted?.(id, incomingStream);
713
+ console.log(`Screen share stream detected for ${id}`);
714
+ } else if (isCameraStream) {
715
+ this.state.updateParticipantMedia(id, {
716
+ stream: incomingStream,
717
+ cameraTrack: incomingStream.getVideoTracks()[0],
718
+ audioTrack: incomingStream.getAudioTracks()[0]
719
+ });
720
+ this.events.onTrack?.(incomingStream, id);
721
+ } else {
722
+ this.state.updateParticipantMedia(id, {
723
+ stream: incomingStream,
724
+ cameraTrack: incomingStream.getVideoTracks()[0],
725
+ audioTrack: incomingStream.getAudioTracks()[0]
726
+ });
727
+ this.events.onTrack?.(incomingStream, id);
728
+ }
729
+ };
730
+ pc.onicecandidate = (e) => {
731
+ if (!e.candidate) return;
732
+ console.log("LOCAL ICE:", e.candidate.candidate);
733
+ this.send({
734
+ type: "ICE",
735
+ payload: JSON.stringify(e.candidate),
736
+ sender: this.myId,
737
+ target: id
738
+ });
739
+ };
740
+ pc.oniceconnectionstatechange = async () => {
741
+ console.log("================================");
742
+ console.log(`Peer: ${id}`);
743
+ console.log(`ICE: ${pc.iceConnectionState}`);
744
+ console.log(`Connection: ${pc.connectionState}`);
745
+ console.log(`Signaling: ${pc.signalingState}`);
746
+ if (pc.iceConnectionState === "disconnected") {
747
+ const stats = await pc.getStats();
748
+ stats.forEach((report) => {
749
+ if (report.type === "transport") {
750
+ console.log(JSON.stringify(report, null, 2));
751
+ }
752
+ });
753
+ }
754
+ };
755
+ pc.onconnectionstatechange = () => {
756
+ if (pc.connectionState === "failed") {
757
+ try {
758
+ pc.restartIce();
759
+ } catch {
760
+ }
761
+ }
762
+ };
763
+ this.localStream.getTracks().forEach((track) => {
764
+ pc.addTrack(track, this.localStream);
765
+ });
766
+ if (this.isScreenSharing && this.screenStream) {
767
+ this.screenSenders[id] = [];
768
+ this.screenStream.getTracks().forEach((track) => {
769
+ const sender = pc.addTrack(track, this.screenStream);
770
+ this.screenSenders[id].push(sender);
771
+ });
772
+ }
773
+ return pc;
774
+ }
775
+ // ---------------- OFFER ----------------
776
+ async createOffer(id, isRenegotiation = false) {
777
+ if (!isRenegotiation && !this.shouldInitiate(id)) {
778
+ console.debug(
779
+ `[Offer] ${id} should initiate (${id} > ${this.myId}), skipping`
780
+ );
781
+ return;
782
+ }
783
+ if (!isRenegotiation && this.initiators.has(id)) {
784
+ console.debug(
785
+ `[Offer] Already initiating with ${id}, skipping duplicate`
786
+ );
787
+ return;
788
+ }
789
+ if (!isRenegotiation) {
790
+ this.initiators.add(id);
791
+ }
792
+ if (!this.peers[id]) {
793
+ this.peers[id] = this.createPeer(id);
794
+ }
795
+ const pc = this.peers[id];
796
+ try {
797
+ const offer = await pc.createOffer();
798
+ await pc.setLocalDescription(offer);
799
+ this.send({
800
+ type: "OFFER",
801
+ payload: offer.sdp,
802
+ sender: this.myId,
803
+ target: id
804
+ });
805
+ console.debug(`[Offer] Sent to ${id}`);
806
+ } catch (err) {
807
+ console.error(`[Offer] Failed for ${id}:`, err);
808
+ }
809
+ }
810
+ shouldInitiate(peerId) {
811
+ return this.myId < peerId;
812
+ }
813
+ // ---------------- ANSWER ----------------
814
+ async handleOffer(sdp, id) {
815
+ if (!this.iceServers || this.iceServers.length === 0) {
816
+ console.warn("[Offer] Waiting for iceServers, queuing offer from", id);
817
+ this.pendingOffers[id] = sdp;
818
+ return;
819
+ }
820
+ if (!this.peers[id]) {
821
+ this.peers[id] = this.createPeer(id);
822
+ }
823
+ const pc = this.peers[id];
824
+ try {
825
+ if (pc.signalingState === "have-local-offer") {
826
+ if (this.shouldInitiate(id)) {
827
+ console.warn(
828
+ `[Glare] Both sent OFFERs, we win (${this.myId} < ${id}), keeping our OFFER`
829
+ );
830
+ return;
831
+ } else {
832
+ console.warn(
833
+ `[Glare] Both sent OFFERs, they win (${id} < ${this.myId}), rolling back`
834
+ );
835
+ pc.close();
836
+ delete this.peers[id];
837
+ this.initiators.delete(id);
838
+ this.peers[id] = this.createPeer(id);
839
+ }
840
+ }
841
+ if (this.peers[id].signalingState !== "stable" && this.peers[id].signalingState !== "have-local-offer") {
842
+ console.warn(
843
+ `[Signaling] Cannot accept OFFER in state "${this.peers[id].signalingState}"`
844
+ );
845
+ return;
846
+ }
847
+ await this.peers[id].setRemoteDescription({
848
+ type: "offer",
849
+ sdp
850
+ });
851
+ const pending = this.pendingIceCandidates[id] || [];
852
+ for (const candidate of pending) {
853
+ try {
854
+ await this.peers[id].addIceCandidate(candidate);
855
+ } catch (err) {
856
+ console.warn("[ICE] Failed to add candidate:", err);
857
+ }
858
+ }
859
+ delete this.pendingIceCandidates[id];
860
+ const answer = await this.peers[id].createAnswer();
861
+ await this.peers[id].setLocalDescription(answer);
862
+ await this.flushIce(id, this.peers[id]);
863
+ this.send({
864
+ type: "ANSWER",
865
+ payload: answer.sdp,
866
+ sender: this.myId,
867
+ target: id
868
+ });
869
+ console.debug(`[Answer] Sent to ${id}`);
870
+ } catch (err) {
871
+ console.error(`[Signaling] Failed to handle OFFER from ${id}:`, err);
872
+ this.emitError(
873
+ "OFFER_HANDLING_FAILED",
874
+ `Failed to handle offer from ${id}`,
875
+ err,
876
+ true
877
+ );
878
+ }
879
+ }
880
+ // ---------------- CLEANUP ----------------
881
+ closePeer(id) {
882
+ const pc = this.peers[id];
883
+ if (!pc) return;
884
+ pc.ontrack = null;
885
+ pc.onicecandidate = null;
886
+ pc.onconnectionstatechange = null;
887
+ pc.close();
888
+ delete this.peers[id];
889
+ this.initiators.delete(id);
890
+ this.state.removeParticipant(id);
891
+ }
892
+ async startScreenShare() {
893
+ try {
894
+ if (this.state.presenterId && this.state.presenterId !== this.myId) {
895
+ throw new Error("Another user is already sharing their screen.");
896
+ }
897
+ if (!navigator.mediaDevices?.getDisplayMedia) {
898
+ throw new Error("Screen sharing not supported on this device");
899
+ }
900
+ this.screenStream = await navigator.mediaDevices.getDisplayMedia({
901
+ video: true
902
+ // audio: true,
903
+ });
904
+ this.isScreenSharing = true;
905
+ this.state.updateLocalParticipant({
906
+ media: {
907
+ isScreenSharing: true,
908
+ screenStream: this.screenStream,
909
+ screenTrack: this.screenStream.getVideoTracks()[0]
910
+ }
911
+ });
912
+ this.state.setPresenterId(this.myId);
913
+ this.screenStream.getVideoTracks()[0].onended = () => {
914
+ this.stopScreenShare();
915
+ };
916
+ Object.entries(this.peers).forEach(([peerId, pc]) => {
917
+ this.screenSenders[peerId] = [];
918
+ this.screenStream.getTracks().forEach((track) => {
919
+ const sender = pc.addTrack(track, this.screenStream);
920
+ this.screenSenders[peerId].push(sender);
921
+ });
922
+ this.createOffer(peerId, true);
923
+ });
924
+ this.send({
925
+ type: "SCREEN_SHARE_START",
926
+ sender: this.myId,
927
+ room_id: this.room.id,
928
+ camera_id: this.localStream?.id.replace(/[{}]/g, ""),
929
+ stream_id: this.screenStream.id.replace(/[{}]/g, "")
930
+ });
931
+ return this.screenStream;
932
+ } catch (err) {
933
+ this.emitError(
934
+ "SCREEN_SHARE_FAILED",
935
+ err?.message || "Failed to start screen sharing",
936
+ err,
937
+ true
938
+ );
939
+ this.isScreenSharing = false;
940
+ this.screenStream = null;
941
+ throw err;
942
+ }
943
+ }
944
+ stopScreenShare() {
945
+ if (!this.screenStream) return;
946
+ this.screenStream.getTracks().forEach((t) => t.stop());
947
+ Object.entries(this.peers).forEach(([peerId, pc]) => {
948
+ const senders = this.screenSenders[peerId] || [];
949
+ senders.forEach((sender) => {
950
+ try {
951
+ pc.removeTrack(sender);
952
+ } catch (err) {
953
+ console.warn(err);
954
+ }
955
+ });
956
+ delete this.screenSenders[peerId];
957
+ this.createOffer(peerId, true);
958
+ });
959
+ this.screenStream = null;
960
+ this.isScreenSharing = false;
961
+ this.state.updateLocalParticipant({
962
+ media: {
963
+ isScreenSharing: false,
964
+ screenStream: null,
965
+ screenTrack: void 0
966
+ }
967
+ });
968
+ if (this.state.presenterId === this.myId) {
969
+ this.state.setPresenterId(null);
970
+ }
971
+ this.send({
972
+ type: "SCREEN_SHARE_STOP",
973
+ sender: this.myId,
974
+ room_id: this.room.id
975
+ });
976
+ }
977
+ sendChatMessage(payload) {
978
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
979
+ console.warn("WS not connected");
980
+ return;
981
+ }
982
+ if (!this.room.id) {
983
+ console.warn("No roomId set");
984
+ return;
985
+ }
986
+ const isPrivate = !!payload?.target;
987
+ const senderName = this.state.localParticipant?.name || "Anonymous";
988
+ const msg = {
989
+ id: crypto.randomUUID(),
990
+ sender_id: this.myId,
991
+ sender_name: senderName,
992
+ text: payload.message.trim(),
993
+ timestamp: Date.now(),
994
+ reply_to: payload.reply_to ?? null,
995
+ target: payload.target ?? null
996
+ };
997
+ this.state.addChatMessage(msg);
998
+ this.send({
999
+ type: "CHAT_MESSAGE",
1000
+ message: payload.message.trim(),
1001
+ user_id: this.myId,
1002
+ sender_name: senderName,
1003
+ room_id: this.room.id,
1004
+ target: isPrivate ? payload.target ?? null : null,
1005
+ reply_to: payload.reply_to ?? null,
1006
+ client_ts: Date.now()
1007
+ });
1008
+ }
1009
+ disconnect() {
1010
+ this.intentionalDisconnect = true;
1011
+ this.stopScreenShare();
1012
+ Object.values(this.peers).forEach((pc) => pc.close());
1013
+ this.peers = {};
1014
+ this.initiators.clear();
1015
+ this.stopHeartbeat();
1016
+ if (this.ws?.readyState === WebSocket.OPEN) {
1017
+ this.send({
1018
+ type: "LEAVE",
1019
+ room_id: this.room.id,
1020
+ user_id: this.myId,
1021
+ sender_name: this.state.localParticipant?.name
1022
+ });
1023
+ setTimeout(() => {
1024
+ this.ws?.close(1e3, "Leaving meeting");
1025
+ this.ws = null;
1026
+ }, 50);
1027
+ }
1028
+ if (this.localStream) {
1029
+ this.localStream.getTracks().forEach((track) => track.stop());
1030
+ this.localStream = null;
1031
+ }
1032
+ this.room.id = null;
1033
+ this.state.localParticipant = null;
1034
+ this.state.notify("localParticipant");
1035
+ this.state.participants.clear();
1036
+ this.state.notify("participants");
1037
+ this.events.onMeetingLeft?.();
1038
+ this.state.clearChat();
1039
+ this.state.setPresenterId(null);
1040
+ }
1041
+ async flushIce(id, pc) {
1042
+ const pending = this.pendingIceCandidates[id];
1043
+ if (!pending?.length) return;
1044
+ for (const candidate of pending) {
1045
+ try {
1046
+ await pc.addIceCandidate(candidate);
1047
+ } catch (e) {
1048
+ console.warn("ICE flush error", e);
1049
+ }
1050
+ }
1051
+ delete this.pendingIceCandidates[id];
1052
+ }
1053
+ send(msg) {
1054
+ this.ws?.send(JSON.stringify(msg));
1055
+ }
1056
+ approveJoinRequest(requestId) {
1057
+ this.send({
1058
+ type: "JOIN_APPROVE",
1059
+ request_id: requestId
1060
+ });
1061
+ }
1062
+ rejectJoinRequest(requestId) {
1063
+ this.send({
1064
+ type: "JOIN_REJECT",
1065
+ request_id: requestId
1066
+ });
1067
+ }
1068
+ };
1069
+
1070
+ // src/react/useMeetingStore.ts
1071
+ import { useEffect, useState } from "react";
1072
+ function useMeetingStore(stateManager, scope, selector) {
1073
+ const [state, setState] = useState(() => selector(stateManager));
1074
+ useEffect(() => {
1075
+ const unsubscribe = stateManager.subscribe(scope, () => {
1076
+ setState(selector(stateManager));
1077
+ });
1078
+ return unsubscribe;
1079
+ }, [stateManager, scope, selector]);
1080
+ return state;
1081
+ }
1082
+
1083
+ // src/react/MeetingProvider.tsx
1084
+ import { jsx } from "react/jsx-runtime";
1085
+ var MeetingContext = createContext(null);
1086
+ var MeetingProvider = ({
1087
+ config,
1088
+ children
1089
+ }) => {
1090
+ const sdkRef = useRef(null);
1091
+ const errorListeners = useRef(/* @__PURE__ */ new Set());
1092
+ const entryRequestListeners = useRef(/* @__PURE__ */ new Set());
1093
+ const entryResponseListeners = useRef(
1094
+ /* @__PURE__ */ new Set()
1095
+ );
1096
+ const meetingLeftListeners = useRef(/* @__PURE__ */ new Set());
1097
+ if (!sdkRef.current) {
1098
+ sdkRef.current = new VideoSDKCore({
1099
+ onError: (err) => errorListeners.current.forEach((fn) => fn(err)),
1100
+ onEntryRequested: (req) => entryRequestListeners.current.forEach((fn) => fn(req)),
1101
+ onEntryResponded: (p, d) => entryResponseListeners.current.forEach((fn) => fn(p, d)),
1102
+ onMeetingLeft: () => meetingLeftListeners.current.forEach((fn) => fn())
1103
+ });
1104
+ }
1105
+ const sdk = sdkRef.current;
1106
+ const presenterId = useMeetingStore(
1107
+ sdk.state,
1108
+ "presenter",
1109
+ (s) => s.presenterId
1110
+ );
1111
+ const participants = useMeetingStore(
1112
+ sdk.state,
1113
+ "participants",
1114
+ (s) => s.participants
1115
+ );
1116
+ const localParticipant = useMeetingStore(
1117
+ sdk.state,
1118
+ "localParticipant",
1119
+ (s) => s.localParticipant
1120
+ );
1121
+ const messages = useMeetingStore(
1122
+ sdk.state,
1123
+ "chat",
1124
+ (s) => s.getChatMessages()
1125
+ );
1126
+ const value = useMemo(() => {
1127
+ if (!sdkRef.current) {
1128
+ sdkRef.current = new VideoSDKCore({
1129
+ onError: (err) => {
1130
+ errorListeners.current.forEach((fn) => fn(err));
1131
+ }
1132
+ });
1133
+ }
1134
+ return {
1135
+ sdk,
1136
+ join: (joinConfig) => sdk.joinMeeting({
1137
+ ...config,
1138
+ ...joinConfig
1139
+ }),
1140
+ leave: () => sdk.disconnect(),
1141
+ toggleMic: sdk.toggleMic.bind(sdk),
1142
+ toggleCam: sdk.toggleCam.bind(sdk),
1143
+ startScreenShare: sdk.startScreenShare.bind(sdk),
1144
+ stopScreenShare: sdk.stopScreenShare.bind(sdk),
1145
+ sendMessage: sdk.sendChatMessage.bind(sdk),
1146
+ room: sdk.getMeeting(),
1147
+ localParticipant,
1148
+ participants,
1149
+ messages,
1150
+ presenterId,
1151
+ usePubSub: (topic) => {
1152
+ if (topic !== "SECURE_CHAT") {
1153
+ throw new Error(`Unsupported PubSub argument: "${topic}"`);
1154
+ }
1155
+ return {
1156
+ messages: sdk.state.getChatMessages(),
1157
+ publish: sdk.sendChatMessage.bind(sdk)
1158
+ };
1159
+ },
1160
+ approveJoinRequest: sdk.approveJoinRequest.bind(sdk),
1161
+ rejectJoinRequest: sdk.rejectJoinRequest.bind(sdk),
1162
+ onError: (cb) => {
1163
+ errorListeners.current.add(cb);
1164
+ return () => {
1165
+ errorListeners.current.delete(cb);
1166
+ };
1167
+ },
1168
+ onEntryRequested: (cb) => {
1169
+ entryRequestListeners.current.add(cb);
1170
+ return () => {
1171
+ entryRequestListeners.current.delete(cb);
1172
+ };
1173
+ },
1174
+ onEntryResponded: (cb) => {
1175
+ entryResponseListeners.current.add(cb);
1176
+ return () => {
1177
+ entryResponseListeners.current.delete(cb);
1178
+ };
1179
+ },
1180
+ onMeetingLeft: (cb) => {
1181
+ meetingLeftListeners.current.add(cb);
1182
+ return () => {
1183
+ meetingLeftListeners.current.delete(cb);
1184
+ };
1185
+ }
1186
+ };
1187
+ }, [config, sdk, localParticipant, participants, messages, presenterId]);
1188
+ return /* @__PURE__ */ jsx(MeetingContext.Provider, { value, children });
1189
+ };
1190
+ var useMeetingContext = () => {
1191
+ const ctx = useContext(MeetingContext);
1192
+ if (!ctx)
1193
+ throw new Error("useMeetingContext must be used inside <MeetingProvider>");
1194
+ return ctx;
1195
+ };
1196
+
1197
+ // src/react/useLocalParticipant.tsx
1198
+ var useLocalParticipant = () => {
1199
+ const { sdk } = useMeetingContext();
1200
+ const [localParticipant, setLocalParticipant] = useState2(
1201
+ () => {
1202
+ const current = sdk.state.localParticipant;
1203
+ return current && current.id ? current : null;
1204
+ }
1205
+ );
1206
+ useEffect2(() => {
1207
+ const unsubscribe = sdk.state.subscribe("localParticipant", () => {
1208
+ const current = sdk.state.localParticipant;
1209
+ if (current && current.id) {
1210
+ setLocalParticipant({ ...current });
1211
+ } else {
1212
+ setLocalParticipant(null);
1213
+ }
1214
+ });
1215
+ return unsubscribe;
1216
+ }, [sdk]);
1217
+ const lastStreamRef = useRef2(null);
1218
+ const videoRef = useCallback(
1219
+ (video) => {
1220
+ if (!video) return;
1221
+ const stream = localParticipant?.media?.stream;
1222
+ if (!stream) return;
1223
+ if (lastStreamRef.current === stream) return;
1224
+ lastStreamRef.current = stream;
1225
+ video.srcObject = stream;
1226
+ video.autoplay = true;
1227
+ video.playsInline = true;
1228
+ video.play().catch((err) => {
1229
+ console.warn(`Autoplay failed for local view:`, err);
1230
+ });
1231
+ },
1232
+ [localParticipant?.media?.stream]
1233
+ );
1234
+ return {
1235
+ participant: localParticipant,
1236
+ videoRef
1237
+ };
1238
+ };
1239
+
1240
+ // src/react/useMeeting.ts
1241
+ import { useEffect as useEffect3 } from "react";
1242
+ var useMeeting = (handlers) => {
1243
+ const ctx = useMeetingContext();
1244
+ useEffect3(() => {
1245
+ if (!handlers?.onError) return;
1246
+ return ctx.onError(handlers.onError);
1247
+ }, [handlers?.onError]);
1248
+ useEffect3(() => {
1249
+ if (!handlers?.onEntryRequested) return;
1250
+ return ctx.onEntryRequested(handlers.onEntryRequested);
1251
+ }, [handlers?.onEntryRequested]);
1252
+ useEffect3(() => {
1253
+ if (!handlers?.onEntryResponded) return;
1254
+ return ctx.onEntryResponded(handlers.onEntryResponded);
1255
+ }, [handlers?.onEntryResponded]);
1256
+ useEffect3(() => {
1257
+ if (!handlers?.onMeetingLeft) return;
1258
+ return ctx.onMeetingLeft(handlers.onMeetingLeft);
1259
+ }, [handlers?.onMeetingLeft]);
1260
+ const { sdk: _, ...publicApi } = ctx;
1261
+ return publicApi;
1262
+ };
1263
+
1264
+ // src/react/useParticipants.ts
1265
+ import { useEffect as useEffect4, useState as useState3 } from "react";
1266
+ var useParticipants = () => {
1267
+ const { sdk } = useMeetingContext();
1268
+ const [participants, setParticipants] = useState3(
1269
+ () => sdk.state.getParticipants()
1270
+ );
1271
+ useEffect4(() => {
1272
+ const update = () => {
1273
+ setParticipants(sdk.state.getParticipants());
1274
+ };
1275
+ update();
1276
+ const unsub = sdk.state.subscribe("participants", update);
1277
+ return unsub;
1278
+ }, [sdk]);
1279
+ return participants;
1280
+ };
1281
+
1282
+ // src/react/useRemoteMedia.ts
1283
+ import { useEffect as useEffect5, useRef as useRef3, useState as useState4 } from "react";
1284
+ var useRemoteMedia = (participantId) => {
1285
+ const { sdk } = useMeetingContext();
1286
+ const videoRef = useRef3(null);
1287
+ const audioRef = useRef3(null);
1288
+ const [participant, setParticipant] = useState4(
1289
+ () => sdk.state.getParticipant(participantId) || null
1290
+ );
1291
+ useEffect5(() => {
1292
+ const unsub = sdk.state.subscribe(`participant:${participantId}`, () => {
1293
+ const p = sdk.state.getParticipant(participantId);
1294
+ setParticipant(p ? { ...p } : null);
1295
+ });
1296
+ return unsub;
1297
+ }, [participantId, sdk]);
1298
+ useEffect5(() => {
1299
+ const stream = participant?.media?.stream;
1300
+ if (!stream) return;
1301
+ if (videoRef.current) {
1302
+ videoRef.current.srcObject = stream;
1303
+ videoRef.current.muted = true;
1304
+ videoRef.current.playsInline = true;
1305
+ videoRef.current.play().catch(() => {
1306
+ });
1307
+ }
1308
+ if (audioRef.current) {
1309
+ audioRef.current.srcObject = stream;
1310
+ audioRef.current.play().catch(() => {
1311
+ });
1312
+ }
1313
+ }, [participant?.media?.stream]);
1314
+ return {
1315
+ videoRef,
1316
+ audioRef,
1317
+ isCamActive: !!participant?.media?.camEnabled,
1318
+ isMicEnabled: !!participant?.media?.micEnabled
1319
+ };
1320
+ };
1321
+
1322
+ // src/react/useMeetingPreview.ts
1323
+ import { useEffect as useEffect6, useState as useState5 } from "react";
1324
+ function useMeetingPreview(roomId, userId) {
1325
+ const [room, setRoom] = useState5(null);
1326
+ const [error, setError] = useState5(null);
1327
+ const [isConnected, setIsConnected] = useState5(false);
1328
+ const [isLoading, setIsLoading] = useState5(true);
1329
+ useEffect6(() => {
1330
+ if (!roomId || !userId) {
1331
+ setIsLoading(false);
1332
+ return;
1333
+ }
1334
+ let ws = null;
1335
+ const heartbeat = setInterval(() => {
1336
+ if (ws && ws.readyState === WebSocket.OPEN) {
1337
+ ws.send(
1338
+ JSON.stringify({
1339
+ type: "PING"
1340
+ })
1341
+ );
1342
+ }
1343
+ }, 2e4);
1344
+ ws = new WebSocket(`${SDK_CONFIG.wsUrl}/watch/${roomId}?user_id=${userId}`);
1345
+ ws.onopen = () => {
1346
+ setIsConnected(true);
1347
+ setError(null);
1348
+ console.log("[Preview] watcher connected");
1349
+ };
1350
+ ws.onmessage = (event) => {
1351
+ try {
1352
+ const msg = JSON.parse(event.data);
1353
+ if (msg.type !== "ROOM_PRESENCE_UPDATE") {
1354
+ return;
1355
+ }
1356
+ setRoom({
1357
+ active: msg.active ?? false,
1358
+ count: msg.count ?? 0,
1359
+ canJoin: msg.canJoin ?? false,
1360
+ approved: msg.approved ?? false,
1361
+ isHost: msg.isHost ?? false,
1362
+ hasMoreParticipants: msg.hasMoreParticipants ?? false,
1363
+ participants: msg.participants ?? []
1364
+ });
1365
+ setIsLoading(false);
1366
+ } catch (err) {
1367
+ console.error("Invalid room presence payload", err);
1368
+ setIsLoading(false);
1369
+ }
1370
+ };
1371
+ ws.onerror = () => {
1372
+ setError("Failed to connect to room monitor");
1373
+ setIsLoading(false);
1374
+ };
1375
+ ws.onclose = () => {
1376
+ setIsConnected(false);
1377
+ console.log("[Preview] disconnected");
1378
+ };
1379
+ return () => {
1380
+ clearInterval(heartbeat);
1381
+ if (ws) {
1382
+ ws.close(1e3, "Leaving preview");
1383
+ }
1384
+ };
1385
+ }, [roomId, userId]);
1386
+ return {
1387
+ room,
1388
+ isConnected,
1389
+ isLoading,
1390
+ error
1391
+ };
1392
+ }
1393
+ export {
1394
+ MeetingProvider,
1395
+ MeetingState,
1396
+ VideoSDKCore,
1397
+ useLocalParticipant,
1398
+ useMeeting,
1399
+ useMeetingContext,
1400
+ useMeetingPreview,
1401
+ useParticipants,
1402
+ useRemoteMedia
1403
+ };
1404
+ //# sourceMappingURL=index.mjs.map