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