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