@afosecure/meetingsdk 1.0.7

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,996 @@
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://rust-video-server-sfyf.onrender.com/ws"
10
+ };
11
+
12
+ // src/core/MeetingState.ts
13
+ var MeetingState = class {
14
+ constructor() {
15
+ this.participants = /* @__PURE__ */ new Map();
16
+ this.localParticipant = null;
17
+ this.localStream = null;
18
+ this.chatMessages = /* @__PURE__ */ new Map();
19
+ this.presenterId = null;
20
+ this.listeners = /* @__PURE__ */ new Map();
21
+ }
22
+ // ---- reactive system ----
23
+ subscribe(scope, fn) {
24
+ if (!this.listeners.has(scope)) {
25
+ this.listeners.set(scope, /* @__PURE__ */ new Set());
26
+ }
27
+ this.listeners.get(scope).add(fn);
28
+ return () => {
29
+ this.listeners.get(scope)?.delete(fn);
30
+ };
31
+ }
32
+ notify(scope) {
33
+ this.listeners.get(scope)?.forEach((fn) => fn());
34
+ }
35
+ setPresenterId(id) {
36
+ if (this.presenterId === id) return;
37
+ this.presenterId = id;
38
+ this.notify("presenter");
39
+ this.notify("participants");
40
+ }
41
+ // ---- participants ----
42
+ addParticipant(p) {
43
+ if (this.participants.has(p.id)) return false;
44
+ const next = new Map(this.participants);
45
+ next.set(p.id, p);
46
+ this.participants = next;
47
+ this.notify("participants");
48
+ return true;
49
+ }
50
+ removeParticipant(id) {
51
+ const next = new Map(this.participants);
52
+ next.delete(id);
53
+ this.participants = next;
54
+ this.notify("participants");
55
+ }
56
+ updateParticipantMedia(id, patch) {
57
+ const p = this.participants.get(id);
58
+ if (!p) return;
59
+ const updated = {
60
+ ...p,
61
+ media: {
62
+ stream: null,
63
+ screenStream: void 0,
64
+ cameraTrack: void 0,
65
+ screenTrack: void 0,
66
+ audioTrack: void 0,
67
+ micEnabled: true,
68
+ camEnabled: true,
69
+ isScreenSharing: false,
70
+ ...p.media,
71
+ // preserve existing media items if they happen to exist
72
+ ...patch
73
+ // apply the incoming stream updates
74
+ }
75
+ };
76
+ const next = new Map(this.participants);
77
+ next.set(id, updated);
78
+ this.participants = next;
79
+ this.notify(`participant:${id}`);
80
+ this.notify("participants");
81
+ }
82
+ updateLocalParticipant(patch) {
83
+ const prev = this.localParticipant;
84
+ if (!prev) {
85
+ this.localParticipant = {
86
+ id: patch.id ?? "",
87
+ name: patch.name ?? "",
88
+ media: {
89
+ stream: patch.media?.stream ?? null,
90
+ // ◄ FIX: Capture the stream from the patch here
91
+ screenStream: patch.media?.screenStream,
92
+ cameraTrack: patch.media?.cameraTrack,
93
+ screenTrack: patch.media?.screenTrack,
94
+ audioTrack: patch.media?.audioTrack,
95
+ micEnabled: patch.media?.micEnabled ?? true,
96
+ camEnabled: patch.media?.camEnabled ?? true,
97
+ isScreenSharing: patch.media?.isScreenSharing ?? false
98
+ }
99
+ };
100
+ this.notify("localParticipant");
101
+ return;
102
+ }
103
+ const prevMedia = prev.media ?? {
104
+ stream: null,
105
+ screenStream: void 0,
106
+ cameraTrack: void 0,
107
+ screenTrack: void 0,
108
+ audioTrack: void 0,
109
+ micEnabled: true,
110
+ camEnabled: true,
111
+ isScreenSharing: false
112
+ };
113
+ const nextMedia = {
114
+ stream: patch.media?.stream ?? prevMedia.stream,
115
+ screenStream: patch.media?.screenStream ?? prevMedia.screenStream,
116
+ cameraTrack: patch.media?.cameraTrack ?? prevMedia.cameraTrack,
117
+ screenTrack: patch.media?.screenTrack ?? prevMedia.screenTrack,
118
+ audioTrack: patch.media?.audioTrack ?? prevMedia.audioTrack,
119
+ micEnabled: patch.media?.micEnabled ?? prevMedia.micEnabled,
120
+ camEnabled: patch.media?.camEnabled ?? prevMedia.camEnabled,
121
+ isScreenSharing: patch.media?.isScreenSharing ?? prevMedia.isScreenSharing
122
+ };
123
+ this.localParticipant = {
124
+ ...prev,
125
+ id: patch.id ?? prev.id,
126
+ name: patch.name ?? prev.name,
127
+ media: nextMedia
128
+ };
129
+ this.notify("localParticipant");
130
+ }
131
+ // ---- chat ----
132
+ addChatMessage(msg) {
133
+ this.chatMessages.set(msg.id, msg);
134
+ this.notify("chat");
135
+ }
136
+ getChatMessages() {
137
+ return Array.from(this.chatMessages.values()).sort(
138
+ (a, b) => a.timestamp - b.timestamp
139
+ );
140
+ }
141
+ clearChat() {
142
+ this.chatMessages.clear();
143
+ this.notify("chat");
144
+ }
145
+ // ---- helpers ----
146
+ getParticipants() {
147
+ return Array.from(this.participants.values());
148
+ }
149
+ getParticipant(id) {
150
+ return this.participants.get(id) ?? null;
151
+ }
152
+ resetRemoteState() {
153
+ this.participants.clear();
154
+ this.chatMessages.clear();
155
+ this.presenterId = null;
156
+ this.notify("participants");
157
+ this.notify("chat");
158
+ this.notify("presenter");
159
+ }
160
+ };
161
+
162
+ // src/core/VideoCore.ts
163
+ var VideoSDKCore = class {
164
+ constructor(events = {}, url = SDK_CONFIG.wsUrl) {
165
+ this.events = events;
166
+ this.url = url;
167
+ this.ws = null;
168
+ this.peers = {};
169
+ this.initiators = /* @__PURE__ */ new Set();
170
+ this.roomId = null;
171
+ this.localStream = null;
172
+ this.screenStream = null;
173
+ this.isScreenSharing = false;
174
+ this.screenSenders = {};
175
+ this.pingInterval = null;
176
+ this.pendingIceCandidates = {};
177
+ this.reconnectAttempts = 0;
178
+ this.participantName = "";
179
+ this.state = new MeetingState();
180
+ this.events = events;
181
+ this.url = url;
182
+ this.myId = localStorage.getItem("vsdk_id") || crypto.randomUUID();
183
+ localStorage.setItem("vsdk_id", this.myId);
184
+ }
185
+ emitError(code, message, raw, recoverable = true) {
186
+ const err = {
187
+ code,
188
+ message,
189
+ raw,
190
+ roomId: this.roomId,
191
+ userId: this.myId,
192
+ recoverable
193
+ };
194
+ this.events.onError?.(err);
195
+ this.joinRejecter?.(err);
196
+ this.joinRejecter = void 0;
197
+ console.error("[MeetingSDK Error]", err);
198
+ }
199
+ // ---------------- STREAM ----------------
200
+ async initLocal(video, name) {
201
+ this.participantName = name;
202
+ if (!this.localStream) {
203
+ this.localStream = await navigator.mediaDevices.getUserMedia({
204
+ video: true,
205
+ audio: true
206
+ });
207
+ }
208
+ video.srcObject = this.localStream;
209
+ this.state.updateLocalParticipant({
210
+ id: this.myId,
211
+ name: this.participantName,
212
+ media: {
213
+ stream: this.localStream,
214
+ micEnabled: true,
215
+ camEnabled: true,
216
+ isScreenSharing: false
217
+ }
218
+ });
219
+ this.state.localStream = this.localStream;
220
+ }
221
+ // ---------------- CONNECT ----------------
222
+ async connect(roomId, name) {
223
+ this.roomId = roomId;
224
+ this.reset();
225
+ return new Promise((resolve, reject) => {
226
+ this.joinResolver = resolve;
227
+ this.joinRejecter = reject;
228
+ this.ws = new WebSocket(this.url);
229
+ this.ws.onopen = () => {
230
+ this.send({
231
+ type: "JOIN",
232
+ room_id: roomId,
233
+ user_id: this.myId,
234
+ sender_name: name
235
+ });
236
+ };
237
+ this.ws.onerror = (err) => {
238
+ this.emitError("WS_ERROR", "WebSocket encountered an error", err, true);
239
+ };
240
+ this.ws.onclose = (e) => {
241
+ this.emitError(
242
+ "WS_CLOSED",
243
+ `Connection closed (${e.code}) ${e.reason || ""}`,
244
+ e,
245
+ true
246
+ );
247
+ this.joinRejecter?.({
248
+ code: "WS_CLOSED",
249
+ message: "Connection closed before join completed",
250
+ raw: e
251
+ });
252
+ this.joinRejecter = void 0;
253
+ this.scheduleReconnect();
254
+ };
255
+ this.ws.onmessage = async (e) => {
256
+ await this.handle(JSON.parse(e.data));
257
+ };
258
+ });
259
+ }
260
+ async joinMeeting(config) {
261
+ const { roomId, name, audioMuted = false, videoMuted = false } = config;
262
+ if (!roomId || !name) {
263
+ throw new Error("roomId and name are required to join meeting");
264
+ }
265
+ this.participantName = name;
266
+ if (!this.localStream) {
267
+ this.localStream = await navigator.mediaDevices.getUserMedia({
268
+ video: true,
269
+ audio: true
270
+ });
271
+ }
272
+ this.localStream.getAudioTracks().forEach((t) => {
273
+ t.enabled = !audioMuted;
274
+ });
275
+ this.localStream.getVideoTracks().forEach((t) => {
276
+ t.enabled = !videoMuted;
277
+ });
278
+ this.state.updateLocalParticipant({
279
+ id: this.myId,
280
+ name: this.participantName,
281
+ media: {
282
+ stream: this.localStream,
283
+ micEnabled: !audioMuted,
284
+ camEnabled: !videoMuted,
285
+ isScreenSharing: false
286
+ }
287
+ });
288
+ this.state.localStream = this.localStream;
289
+ await this.connect(roomId, name);
290
+ }
291
+ /** Expose the roomId without making it fully public */
292
+ getMeetingId() {
293
+ return this.roomId;
294
+ }
295
+ toggleMic() {
296
+ const mediaState = this.state.localParticipant?.media;
297
+ if (!mediaState) return;
298
+ const nextEnabled = !mediaState.micEnabled;
299
+ this.localStream?.getAudioTracks().forEach((t) => t.enabled = nextEnabled);
300
+ this.state.updateLocalParticipant({
301
+ id: this.myId,
302
+ name: this.participantName,
303
+ media: {
304
+ ...mediaState,
305
+ micEnabled: nextEnabled
306
+ }
307
+ });
308
+ this.send({
309
+ type: "MEDIA_STATE",
310
+ kind: "audio",
311
+ enabled: nextEnabled
312
+ });
313
+ }
314
+ toggleCam() {
315
+ const mediaState = this.state.localParticipant?.media;
316
+ if (!mediaState) return;
317
+ const nextEnabled = !mediaState.camEnabled;
318
+ this.localStream?.getVideoTracks().forEach((t) => t.enabled = nextEnabled);
319
+ this.state.updateLocalParticipant({
320
+ id: this.myId,
321
+ name: this.participantName,
322
+ media: {
323
+ ...mediaState,
324
+ camEnabled: nextEnabled
325
+ }
326
+ });
327
+ this.send({
328
+ type: "MEDIA_STATE",
329
+ kind: "video",
330
+ enabled: nextEnabled
331
+ });
332
+ }
333
+ scheduleReconnect() {
334
+ if (!this.roomId) return;
335
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
336
+ clearTimeout(this.reconnectTimer);
337
+ this.reconnectTimer = window.setTimeout(async () => {
338
+ try {
339
+ await this.connect(this.roomId, this.participantName);
340
+ this.reconnectAttempts = 0;
341
+ } catch {
342
+ this.reconnectAttempts++;
343
+ this.scheduleReconnect();
344
+ }
345
+ }, delay);
346
+ }
347
+ startHeartbeat() {
348
+ this.stopHeartbeat();
349
+ this.pingInterval = setInterval(() => {
350
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
351
+ this.send({
352
+ type: "PING",
353
+ client_ts: Date.now()
354
+ });
355
+ }, 2e4);
356
+ }
357
+ stopHeartbeat() {
358
+ if (this.pingInterval) {
359
+ clearInterval(this.pingInterval);
360
+ this.pingInterval = null;
361
+ }
362
+ }
363
+ // ---------------- RESET ----------------
364
+ reset() {
365
+ Object.values(this.peers).forEach((pc) => pc.close());
366
+ this.peers = {};
367
+ this.initiators.clear();
368
+ this.pendingIceCandidates = {};
369
+ this.state.resetRemoteState();
370
+ }
371
+ // ---------------- HANDLE SIGNALS ----------------
372
+ async handle(msg) {
373
+ var _a, _b, _c, _d;
374
+ if (msg.sender === this.myId) return;
375
+ switch (msg.type) {
376
+ case "EXISTING_USERS":
377
+ if (msg.presenterId) {
378
+ this.state.setPresenterId(msg.presenterId);
379
+ this.events.onScreenShareStarted?.(msg.presenterId, null);
380
+ }
381
+ for (const p of msg.participants || []) {
382
+ if (!p?.id || p.id === this.myId) continue;
383
+ this.state.addParticipant(p);
384
+ this.events.onUserJoined?.(p);
385
+ await this.createOffer(p.id);
386
+ }
387
+ break;
388
+ case "JOINED": {
389
+ this.startHeartbeat();
390
+ this.joinResolver?.();
391
+ this.joinResolver = void 0;
392
+ this.joinRejecter = void 0;
393
+ break;
394
+ }
395
+ case "USER_JOINED": {
396
+ const p = msg.participant;
397
+ if (!p?.id || p.id === this.myId) return;
398
+ this.state.addParticipant(p);
399
+ this.events.onUserJoined?.(p);
400
+ break;
401
+ }
402
+ case "OFFER":
403
+ await this.handleOffer(msg.payload, msg.sender);
404
+ break;
405
+ case "ANSWER": {
406
+ const pc = this.peers[msg.sender];
407
+ if (!pc) return;
408
+ if (pc.signalingState !== "have-local-offer") {
409
+ console.warn("Ignoring invalid answer:", pc.signalingState);
410
+ return;
411
+ }
412
+ await pc.setRemoteDescription({
413
+ type: "answer",
414
+ sdp: msg.payload
415
+ });
416
+ await this.flushIce(msg.sender, pc);
417
+ break;
418
+ }
419
+ case "ICE": {
420
+ const candidate = JSON.parse(msg.payload);
421
+ let pc = this.peers[msg.sender];
422
+ if (!pc) {
423
+ (_a = this.pendingIceCandidates)[_b = msg.sender] ?? (_a[_b] = []);
424
+ this.pendingIceCandidates[msg.sender].push(candidate);
425
+ break;
426
+ }
427
+ if (!pc.remoteDescription) {
428
+ (_c = this.pendingIceCandidates)[_d = msg.sender] ?? (_c[_d] = []);
429
+ this.pendingIceCandidates[msg.sender].push(candidate);
430
+ break;
431
+ }
432
+ try {
433
+ await pc.addIceCandidate(candidate);
434
+ } catch (err) {
435
+ console.warn("ICE error:", err);
436
+ }
437
+ break;
438
+ }
439
+ case "USER_LEFT":
440
+ const peerId = msg.participant.id;
441
+ this.closePeer(peerId);
442
+ this.state.removeParticipant(peerId);
443
+ this.events.onUserLeft?.(peerId);
444
+ break;
445
+ case "MEDIA_STATE_CHANGE": {
446
+ const peerId2 = msg.peerId;
447
+ const { kind, enabled } = msg;
448
+ if (kind === "audio") {
449
+ this.state.updateParticipantMedia(peerId2, { micEnabled: enabled });
450
+ this.events.onMicToggled?.(peerId2, enabled);
451
+ } else if (kind === "video") {
452
+ this.state.updateParticipantMedia(peerId2, { camEnabled: enabled });
453
+ this.events.onCamToggled?.(peerId2, enabled);
454
+ }
455
+ break;
456
+ }
457
+ case "CHAT_MESSAGE": {
458
+ const newMsg = msg.data;
459
+ if (newMsg.sender_id === this.myId) break;
460
+ this.state.addChatMessage({
461
+ id: newMsg.id,
462
+ text: newMsg.message,
463
+ sender_id: newMsg.sender_id,
464
+ sender_name: newMsg.sender_name,
465
+ timestamp: new Date(newMsg.timestamp).getTime(),
466
+ target: newMsg.target
467
+ });
468
+ this.events.onChatMessage?.(msg);
469
+ break;
470
+ }
471
+ case "SCREEN_SHARE_START": {
472
+ const peerId2 = msg.peerId;
473
+ this.state.updateParticipantMedia(peerId2, {
474
+ isScreenSharing: true,
475
+ remoteScreenStreamId: msg.stream_id
476
+ });
477
+ if (!this.state.presenterId) {
478
+ this.state.setPresenterId(peerId2);
479
+ }
480
+ const screenStream = this.state.getParticipant(peerId2)?.media?.screenStream;
481
+ this.events.onScreenShareStarted?.(peerId2, screenStream || null);
482
+ break;
483
+ }
484
+ case "SCREEN_SHARE_STOP": {
485
+ const peerId2 = msg.peerId;
486
+ this.state.updateParticipantMedia(peerId2, { isScreenSharing: false });
487
+ if (this.state.presenterId === peerId2) {
488
+ this.state.setPresenterId(null);
489
+ }
490
+ this.events.onScreenShareStopped?.(peerId2);
491
+ break;
492
+ }
493
+ case "ERROR": {
494
+ const fatal = msg?.fatal === true;
495
+ this.emitError(
496
+ "WS_ERROR",
497
+ msg?.message || "Unknown error",
498
+ msg,
499
+ !fatal
500
+ );
501
+ if (fatal) {
502
+ this.disconnect();
503
+ }
504
+ return;
505
+ }
506
+ }
507
+ }
508
+ // ---------------- PEER ----------------
509
+ createPeer(id) {
510
+ if (!this.localStream) throw new Error("No local stream");
511
+ const pc = new RTCPeerConnection({
512
+ iceServers: [
513
+ {
514
+ urls: [
515
+ "stun:stun.l.google.com:19302",
516
+ "stun:stun1.l.google.com:19302"
517
+ ]
518
+ }
519
+ ]
520
+ });
521
+ pc.ontrack = (event) => {
522
+ const incomingStream = event.streams[0];
523
+ const participant = this.state.getParticipant(id);
524
+ const isScreenStream = incomingStream.id === participant?.media?.remoteScreenStreamId;
525
+ if (isScreenStream) {
526
+ const videoTrack = event.track.kind === "video" ? event.track : incomingStream.getVideoTracks()[0] || participant?.media?.screenTrack;
527
+ this.state.updateParticipantMedia(id, {
528
+ screenStream: incomingStream,
529
+ screenTrack: videoTrack,
530
+ isScreenSharing: true
531
+ });
532
+ if (!this.state.presenterId) {
533
+ this.state.setPresenterId(id);
534
+ }
535
+ this.events.onScreenShareStarted?.(id, incomingStream);
536
+ } else {
537
+ this.state.updateParticipantMedia(id, {
538
+ stream: incomingStream,
539
+ cameraTrack: incomingStream.getVideoTracks()[0]
540
+ });
541
+ this.events.onTrack?.(incomingStream, id);
542
+ }
543
+ };
544
+ pc.onicecandidate = (e) => {
545
+ if (!e.candidate) return;
546
+ this.send({
547
+ type: "ICE",
548
+ payload: JSON.stringify(e.candidate),
549
+ sender: this.myId,
550
+ target: id
551
+ });
552
+ };
553
+ pc.onconnectionstatechange = () => {
554
+ if (pc.connectionState === "failed") {
555
+ try {
556
+ pc.restartIce();
557
+ } catch {
558
+ }
559
+ }
560
+ };
561
+ this.localStream.getTracks().forEach((track) => {
562
+ pc.addTrack(track, this.localStream);
563
+ });
564
+ if (this.isScreenSharing && this.screenStream) {
565
+ this.screenSenders[id] = [];
566
+ this.screenStream.getTracks().forEach((track) => {
567
+ const sender = pc.addTrack(track, this.screenStream);
568
+ this.screenSenders[id].push(sender);
569
+ });
570
+ }
571
+ return pc;
572
+ }
573
+ // ---------------- OFFER ----------------
574
+ async createOffer(id, isRenegotiation = false) {
575
+ if (!isRenegotiation && this.initiators.has(id)) return;
576
+ if (!isRenegotiation) {
577
+ this.initiators.add(id);
578
+ }
579
+ if (!this.peers[id]) {
580
+ this.peers[id] = this.createPeer(id);
581
+ }
582
+ const pc = this.peers[id];
583
+ if (pc.signalingState !== "stable") {
584
+ return;
585
+ }
586
+ const offer = await pc.createOffer();
587
+ await pc.setLocalDescription(offer);
588
+ this.send({
589
+ type: "OFFER",
590
+ payload: offer.sdp,
591
+ sender: this.myId,
592
+ target: id
593
+ });
594
+ }
595
+ // ---------------- ANSWER ----------------
596
+ async handleOffer(sdp, id) {
597
+ if (!this.peers[id]) {
598
+ this.peers[id] = this.createPeer(id);
599
+ }
600
+ const pc = this.peers[id];
601
+ await pc.setRemoteDescription({
602
+ type: "offer",
603
+ sdp
604
+ });
605
+ const pending = this.pendingIceCandidates[id] || [];
606
+ for (const candidate of pending) {
607
+ try {
608
+ await pc.addIceCandidate(candidate);
609
+ } catch (err) {
610
+ console.warn(err);
611
+ }
612
+ }
613
+ delete this.pendingIceCandidates[id];
614
+ const answer = await pc.createAnswer();
615
+ await pc.setLocalDescription(answer);
616
+ await this.flushIce(id, pc);
617
+ this.send({
618
+ type: "ANSWER",
619
+ payload: answer.sdp,
620
+ sender: this.myId,
621
+ target: id
622
+ });
623
+ }
624
+ // ---------------- CLEANUP ----------------
625
+ closePeer(id) {
626
+ const pc = this.peers[id];
627
+ if (!pc) return;
628
+ pc.ontrack = null;
629
+ pc.onicecandidate = null;
630
+ pc.onconnectionstatechange = null;
631
+ pc.close();
632
+ delete this.peers[id];
633
+ this.initiators.delete(id);
634
+ this.state.removeParticipant(id);
635
+ }
636
+ async startScreenShare() {
637
+ if (this.state.presenterId && this.state.presenterId !== this.myId) {
638
+ throw new Error("Another user is already sharing their screen.");
639
+ }
640
+ this.screenStream = await navigator.mediaDevices.getDisplayMedia({
641
+ video: true,
642
+ audio: true
643
+ });
644
+ this.isScreenSharing = true;
645
+ this.state.updateLocalParticipant({
646
+ media: {
647
+ isScreenSharing: true,
648
+ screenStream: this.screenStream
649
+ }
650
+ });
651
+ this.state.setPresenterId(this.myId);
652
+ this.screenStream.getVideoTracks()[0].onended = () => {
653
+ this.stopScreenShare();
654
+ };
655
+ Object.entries(this.peers).forEach(([peerId, pc]) => {
656
+ this.screenSenders[peerId] = [];
657
+ this.screenStream.getTracks().forEach((track) => {
658
+ const sender = pc.addTrack(track, this.screenStream);
659
+ this.screenSenders[peerId].push(sender);
660
+ });
661
+ this.createOffer(peerId, true);
662
+ });
663
+ this.send({
664
+ type: "SCREEN_SHARE_START",
665
+ sender: this.myId,
666
+ room_id: this.roomId,
667
+ stream_id: this.screenStream.id.replace(/[{}]/g, "")
668
+ });
669
+ return this.screenStream;
670
+ }
671
+ stopScreenShare() {
672
+ if (!this.screenStream) return;
673
+ this.screenStream.getTracks().forEach((t) => t.stop());
674
+ Object.entries(this.peers).forEach(([peerId, pc]) => {
675
+ const senders = this.screenSenders[peerId] || [];
676
+ senders.forEach((sender) => {
677
+ try {
678
+ pc.removeTrack(sender);
679
+ } catch (err) {
680
+ console.warn(err);
681
+ }
682
+ });
683
+ delete this.screenSenders[peerId];
684
+ this.createOffer(peerId, true);
685
+ });
686
+ this.screenStream = null;
687
+ this.isScreenSharing = false;
688
+ this.state.updateLocalParticipant({
689
+ media: {
690
+ isScreenSharing: false,
691
+ screenStream: null,
692
+ screenTrack: void 0
693
+ }
694
+ });
695
+ if (this.state.presenterId === this.myId) {
696
+ this.state.setPresenterId(null);
697
+ }
698
+ this.send({
699
+ type: "SCREEN_SHARE_STOP",
700
+ sender: this.myId,
701
+ room_id: this.roomId
702
+ });
703
+ }
704
+ sendChatMessage(payload) {
705
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
706
+ console.warn("WS not connected");
707
+ return;
708
+ }
709
+ if (!this.roomId) {
710
+ console.warn("No roomId set");
711
+ return;
712
+ }
713
+ const isPrivate = !!payload?.target;
714
+ const senderName = this.state.localParticipant?.name || "Anonymous";
715
+ const msg = {
716
+ id: crypto.randomUUID(),
717
+ sender_id: this.myId,
718
+ sender_name: senderName,
719
+ text: payload.message.trim(),
720
+ timestamp: Date.now(),
721
+ reply_to: payload.reply_to ?? null,
722
+ target: payload.target ?? null
723
+ };
724
+ this.state.addChatMessage(msg);
725
+ this.send({
726
+ type: "CHAT_MESSAGE",
727
+ message: payload.message.trim(),
728
+ user_id: this.myId,
729
+ sender_name: senderName,
730
+ room_id: this.roomId,
731
+ target: isPrivate ? payload.target ?? null : null,
732
+ reply_to: payload.reply_to ?? null,
733
+ client_ts: Date.now()
734
+ });
735
+ }
736
+ disconnect() {
737
+ this.stopScreenShare();
738
+ Object.values(this.peers).forEach((pc) => pc.close());
739
+ this.peers = {};
740
+ this.initiators.clear();
741
+ this.stopHeartbeat();
742
+ if (this.ws) {
743
+ this.ws.close();
744
+ this.ws = null;
745
+ }
746
+ if (this.localStream) {
747
+ this.localStream.getTracks().forEach((track) => track.stop());
748
+ this.localStream = null;
749
+ }
750
+ this.roomId = null;
751
+ this.state.localParticipant = null;
752
+ this.state.notify("localParticipant");
753
+ this.state.participants.clear();
754
+ this.state.notify("participants");
755
+ this.state.clearChat();
756
+ this.state.setPresenterId(null);
757
+ }
758
+ async flushIce(id, pc) {
759
+ const pending = this.pendingIceCandidates[id];
760
+ if (!pending?.length) return;
761
+ for (const candidate of pending) {
762
+ try {
763
+ await pc.addIceCandidate(candidate);
764
+ } catch (e) {
765
+ console.warn("ICE flush error", e);
766
+ }
767
+ }
768
+ delete this.pendingIceCandidates[id];
769
+ }
770
+ send(msg) {
771
+ this.ws?.send(JSON.stringify(msg));
772
+ }
773
+ };
774
+
775
+ // src/react/useMeetingStore.ts
776
+ import { useEffect, useState } from "react";
777
+ function useMeetingStore(stateManager, scope, selector) {
778
+ const [state, setState] = useState(() => selector(stateManager));
779
+ useEffect(() => {
780
+ const unsubscribe = stateManager.subscribe(scope, () => {
781
+ setState(selector(stateManager));
782
+ });
783
+ return unsubscribe;
784
+ }, [stateManager, scope, selector]);
785
+ return state;
786
+ }
787
+
788
+ // src/react/MeetingProvider.tsx
789
+ import { jsx } from "react/jsx-runtime";
790
+ var MeetingContext = createContext(null);
791
+ var MeetingProvider = ({
792
+ config,
793
+ children
794
+ }) => {
795
+ const sdkRef = useRef(null);
796
+ const errorListeners = useRef(/* @__PURE__ */ new Set());
797
+ if (!sdkRef.current) {
798
+ sdkRef.current = new VideoSDKCore({
799
+ onError: (err) => {
800
+ errorListeners.current.forEach((fn) => fn(err));
801
+ }
802
+ });
803
+ }
804
+ const sdk = sdkRef.current;
805
+ const presenterId = useMeetingStore(
806
+ sdk.state,
807
+ "presenter",
808
+ (s) => s.presenterId
809
+ );
810
+ const participants = useMeetingStore(
811
+ sdk.state,
812
+ "participants",
813
+ (s) => s.participants
814
+ );
815
+ const localParticipant = useMeetingStore(
816
+ sdk.state,
817
+ "localParticipant",
818
+ (s) => s.localParticipant
819
+ );
820
+ const messages = useMeetingStore(
821
+ sdk.state,
822
+ "chat",
823
+ (s) => s.getChatMessages()
824
+ );
825
+ const value = useMemo(() => {
826
+ if (!sdkRef.current) {
827
+ sdkRef.current = new VideoSDKCore({
828
+ onError: (err) => {
829
+ errorListeners.current.forEach((fn) => fn(err));
830
+ }
831
+ });
832
+ }
833
+ return {
834
+ sdk,
835
+ join: (joinConfig) => sdk.joinMeeting({
836
+ ...config,
837
+ ...joinConfig
838
+ }),
839
+ leave: () => sdk.disconnect(),
840
+ toggleMic: sdk.toggleMic.bind(sdk),
841
+ toggleCam: sdk.toggleCam.bind(sdk),
842
+ startScreenShare: sdk.startScreenShare.bind(sdk),
843
+ stopScreenShare: sdk.stopScreenShare.bind(sdk),
844
+ sendMessage: sdk.sendChatMessage.bind(sdk),
845
+ meetingId: sdk.getMeetingId(),
846
+ localParticipant,
847
+ participants,
848
+ messages,
849
+ presenterId,
850
+ usePubSub: (topic) => {
851
+ if (topic !== "SECURE_CHAT") {
852
+ throw new Error(`Unsupported PubSub argument: "${topic}"`);
853
+ }
854
+ return {
855
+ messages: sdk.state.getChatMessages(),
856
+ publish: sdk.sendChatMessage.bind(sdk)
857
+ };
858
+ },
859
+ onError: (cb) => {
860
+ errorListeners.current.add(cb);
861
+ return () => {
862
+ errorListeners.current.delete(cb);
863
+ };
864
+ }
865
+ };
866
+ }, [config, sdk, localParticipant, participants, messages, presenterId]);
867
+ return /* @__PURE__ */ jsx(MeetingContext.Provider, { value, children });
868
+ };
869
+ var useMeetingContext = () => {
870
+ const ctx = useContext(MeetingContext);
871
+ if (!ctx)
872
+ throw new Error("useMeetingContext must be used inside <MeetingProvider>");
873
+ return ctx;
874
+ };
875
+
876
+ // src/react/useLocalParticipant.tsx
877
+ var useLocalParticipant = () => {
878
+ const { sdk } = useMeetingContext();
879
+ const [localParticipant, setLocalParticipant] = useState2(
880
+ () => {
881
+ const current = sdk.state.localParticipant;
882
+ return current && current.id ? current : null;
883
+ }
884
+ );
885
+ useEffect2(() => {
886
+ const unsubscribe = sdk.state.subscribe("localParticipant", () => {
887
+ const current = sdk.state.localParticipant;
888
+ if (current && current.id) {
889
+ setLocalParticipant({ ...current });
890
+ } else {
891
+ setLocalParticipant(null);
892
+ }
893
+ });
894
+ return unsubscribe;
895
+ }, [sdk]);
896
+ const lastStreamRef = useRef2(null);
897
+ const videoRef = useCallback(
898
+ (video) => {
899
+ if (!video) return;
900
+ const stream = localParticipant?.media?.stream;
901
+ if (!stream) return;
902
+ if (lastStreamRef.current === stream) return;
903
+ lastStreamRef.current = stream;
904
+ video.srcObject = stream;
905
+ video.autoplay = true;
906
+ video.playsInline = true;
907
+ video.muted = true;
908
+ video.play().catch((err) => {
909
+ console.warn(`Autoplay failed for local view:`, err);
910
+ });
911
+ },
912
+ [localParticipant?.media?.stream]
913
+ );
914
+ return {
915
+ participant: localParticipant,
916
+ videoRef
917
+ };
918
+ };
919
+
920
+ // src/react/useMeeting.ts
921
+ import { useEffect as useEffect3 } from "react";
922
+ var useMeeting = (handlers) => {
923
+ const ctx = useMeetingContext();
924
+ useEffect3(() => {
925
+ if (!handlers?.onError) return;
926
+ const unsubscribe = ctx.onError(handlers.onError);
927
+ return unsubscribe;
928
+ }, [handlers?.onError]);
929
+ return ctx;
930
+ };
931
+
932
+ // src/react/useParticipants.ts
933
+ import { useEffect as useEffect4, useState as useState3 } from "react";
934
+ var useParticipants = () => {
935
+ const { sdk } = useMeetingContext();
936
+ const [participants, setParticipants] = useState3(
937
+ () => sdk.state.getParticipants()
938
+ );
939
+ useEffect4(() => {
940
+ const update = () => {
941
+ setParticipants(sdk.state.getParticipants());
942
+ };
943
+ update();
944
+ const unsub = sdk.state.subscribe("participants", update);
945
+ return unsub;
946
+ }, [sdk]);
947
+ return participants;
948
+ };
949
+
950
+ // src/react/useRemoteMedia.ts
951
+ import { useEffect as useEffect5, useRef as useRef3, useState as useState4 } from "react";
952
+ var useRemoteMedia = (participantId) => {
953
+ const { sdk } = useMeetingContext();
954
+ const videoRef = useRef3(null);
955
+ const audioRef = useRef3(null);
956
+ const [participant, setParticipant] = useState4(
957
+ () => sdk.state.getParticipant(participantId) || null
958
+ );
959
+ useEffect5(() => {
960
+ const unsub = sdk.state.subscribe(`participant:${participantId}`, () => {
961
+ const updated = sdk.state.getParticipant(participantId);
962
+ if (updated) {
963
+ setParticipant({ ...updated });
964
+ }
965
+ });
966
+ return unsub;
967
+ }, [participantId, sdk]);
968
+ useEffect5(() => {
969
+ const stream = participant?.media?.stream;
970
+ if (stream) {
971
+ if (videoRef.current && videoRef.current.srcObject !== stream) {
972
+ videoRef.current.srcObject = stream;
973
+ }
974
+ if (audioRef.current && audioRef.current.srcObject !== stream) {
975
+ audioRef.current.srcObject = stream;
976
+ }
977
+ }
978
+ }, [participant?.media?.stream]);
979
+ return {
980
+ videoRef,
981
+ audioRef,
982
+ isCamActive: !!participant?.media?.camEnabled,
983
+ isMicEnabled: !!participant?.media?.micEnabled
984
+ };
985
+ };
986
+ export {
987
+ MeetingProvider,
988
+ MeetingState,
989
+ VideoSDKCore,
990
+ useLocalParticipant,
991
+ useMeeting,
992
+ useMeetingContext,
993
+ useParticipants,
994
+ useRemoteMedia
995
+ };
996
+ //# sourceMappingURL=index.mjs.map