@afosecure/meetingsdk 1.4.4 → 1.4.5

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 CHANGED
@@ -198,10 +198,10 @@ var VideoSDKCore = class {
198
198
  this.events = events;
199
199
  this.url = url;
200
200
  this.ws = null;
201
- this.peers = {};
202
- this.initiators = /* @__PURE__ */ new Set();
203
- this.lastPong = Date.now();
201
+ this.pubPC = null;
202
+ this.subPC = null;
204
203
  this.iceServers = [];
204
+ this.lastPong = Date.now();
205
205
  this.intentionalDisconnect = false;
206
206
  this.room = {
207
207
  id: null,
@@ -209,14 +209,11 @@ var VideoSDKCore = class {
209
209
  };
210
210
  this.localStream = null;
211
211
  this.screenStream = null;
212
+ this.screenSender = null;
212
213
  this.isScreenSharing = false;
213
- this.screenSenders = {};
214
214
  this.pingInterval = null;
215
- this.pendingIceCandidates = {};
216
- this.pendingOffers = {};
217
215
  this.reconnectAttempts = 0;
218
216
  this.participantName = "";
219
- // Track if we're in the waiting room (pending approval)
220
217
  this.isWaitingForApproval = false;
221
218
  this.pendingRequestId = null;
222
219
  this.iceTransportPolicy = "all";
@@ -226,29 +223,12 @@ var VideoSDKCore = class {
226
223
  this.myId = localStorage.getItem("vsdk_id") || crypto.randomUUID();
227
224
  localStorage.setItem("vsdk_id", this.myId);
228
225
  }
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 ----------------
226
+ // ---------------- MEDIA SETUP ----------------
244
227
  async initLocal(video, name) {
245
228
  this.participantName = name;
246
229
  try {
247
230
  this.localStream = await navigator.mediaDevices.getUserMedia({
248
- video: {
249
- width: { ideal: 1280 },
250
- height: { ideal: 720 }
251
- },
231
+ video: { width: { ideal: 1280 }, height: { ideal: 720 } },
252
232
  audio: {
253
233
  echoCancellation: true,
254
234
  noiseSuppression: true,
@@ -277,7 +257,115 @@ var VideoSDKCore = class {
277
257
  throw err;
278
258
  }
279
259
  }
280
- // ---------------- CONNECT ----------------
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) => t.enabled = !audioMuted);
273
+ this.localStream.getVideoTracks().forEach((t) => t.enabled = !videoMuted);
274
+ this.state.updateLocalParticipant({
275
+ id: this.myId,
276
+ name: this.participantName,
277
+ media: {
278
+ stream: this.localStream,
279
+ micEnabled: !audioMuted,
280
+ camEnabled: !videoMuted,
281
+ isScreenSharing: false
282
+ }
283
+ });
284
+ this.state.localStream = this.localStream;
285
+ await this.connect(roomId, name);
286
+ }
287
+ // ---------------- SFU PEER CONNECTION CREATION ----------------
288
+ setupPublisherPC() {
289
+ if (!this.localStream) return;
290
+ this.pubPC = new RTCPeerConnection({
291
+ iceServers: this.iceServers,
292
+ iceTransportPolicy: this.iceTransportPolicy
293
+ });
294
+ this.localStream.getTracks().forEach((track) => {
295
+ this.pubPC?.addTrack(track, this.localStream);
296
+ });
297
+ this.pubPC.onicecandidate = (e) => {
298
+ if (e.candidate) {
299
+ this.send({
300
+ type: "PUB_ICE",
301
+ payload: JSON.stringify(e.candidate),
302
+ user_id: this.myId
303
+ });
304
+ }
305
+ };
306
+ this.pubPC.onconnectionstatechange = () => {
307
+ console.log(`[SFU Publisher PC State]`, this.pubPC?.connectionState);
308
+ if (this.pubPC?.connectionState === "failed") {
309
+ this.restartPublisherIce();
310
+ }
311
+ };
312
+ }
313
+ setupSubscriberPC() {
314
+ this.subPC = new RTCPeerConnection({
315
+ iceServers: this.iceServers,
316
+ iceTransportPolicy: this.iceTransportPolicy
317
+ });
318
+ this.subPC.onicecandidate = (e) => {
319
+ if (e.candidate) {
320
+ this.send({
321
+ type: "SUB_ICE",
322
+ payload: JSON.stringify(e.candidate),
323
+ user_id: this.myId
324
+ });
325
+ }
326
+ };
327
+ this.subPC.ontrack = (event) => {
328
+ const incomingStream = event.streams[0] || new MediaStream([event.track]);
329
+ const streamId = incomingStream.id.replace(/[{}]/g, "");
330
+ let matchedParticipant;
331
+ for (const p of this.state.participants.values()) {
332
+ if (p.media?.cameraStreamId === streamId || p.media?.remoteScreenStreamId === streamId) {
333
+ matchedParticipant = p;
334
+ break;
335
+ }
336
+ }
337
+ if (!matchedParticipant) {
338
+ console.warn(
339
+ `[SFU ontrack] Dynamic track received for stream ${streamId}`
340
+ );
341
+ return;
342
+ }
343
+ const pId = matchedParticipant.id;
344
+ const isScreen = streamId === matchedParticipant.media?.remoteScreenStreamId;
345
+ if (isScreen) {
346
+ this.state.updateParticipantMedia(pId, {
347
+ screenStream: incomingStream,
348
+ screenTrack: event.track,
349
+ isScreenSharing: true
350
+ });
351
+ if (!this.state.presenterId) {
352
+ this.state.setPresenterId(pId);
353
+ }
354
+ this.events.onScreenShareStarted?.(pId, incomingStream);
355
+ } else {
356
+ this.state.updateParticipantMedia(pId, {
357
+ stream: incomingStream,
358
+ cameraTrack: incomingStream.getVideoTracks()[0],
359
+ audioTrack: incomingStream.getAudioTracks()[0]
360
+ });
361
+ this.events.onTrack?.(incomingStream, pId);
362
+ }
363
+ };
364
+ this.subPC.onconnectionstatechange = () => {
365
+ console.log(`[SFU Subscriber PC State]`, this.subPC?.connectionState);
366
+ };
367
+ }
368
+ // ---------------- WEBSOCKET CONNECTION & SIGNALING ----------------
281
369
  async connect(roomId, name) {
282
370
  this.room.id = roomId;
283
371
  this.reset();
@@ -286,15 +374,17 @@ var VideoSDKCore = class {
286
374
  this.joinRejecter = reject;
287
375
  this.ws = new WebSocket(this.url);
288
376
  this.ws.onopen = () => {
289
- console.log("WebSocket connected, sending JOIN...");
377
+ console.log("WebSocket connected to SFU, sending JOIN...");
378
+ const micEnabled = !!this.state.localParticipant?.media?.micEnabled;
379
+ const camEnabled = !!this.state.localParticipant?.media?.camEnabled;
290
380
  this.send({
291
381
  type: "JOIN",
292
382
  room_id: roomId,
293
383
  user_id: this.myId,
294
384
  sender_name: name,
295
385
  camera_stream_id: this.localStream?.id.replace(/[{}]/g, ""),
296
- audio_muted: this.state.localParticipant?.media?.micEnabled,
297
- video_muted: this.state.localParticipant?.media?.camEnabled
386
+ audio_muted: !micEnabled,
387
+ video_muted: !camEnabled
298
388
  });
299
389
  };
300
390
  this.ws.onerror = (err) => {
@@ -307,13 +397,7 @@ var VideoSDKCore = class {
307
397
  raw: e
308
398
  });
309
399
  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) {
400
+ if (this.intentionalDisconnect || e.code === 1e3 || e.code === 1001 || this.isWaitingForApproval) {
317
401
  return;
318
402
  }
319
403
  this.scheduleReconnect();
@@ -323,203 +407,70 @@ var VideoSDKCore = class {
323
407
  };
324
408
  });
325
409
  }
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
410
  async handle(msg) {
455
- var _a, _b, _c, _d;
456
411
  if (msg.sender === this.myId) return;
457
412
  switch (msg.type) {
458
413
  case "PONG":
459
414
  this.lastPong = Date.now();
460
415
  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;
416
+ case "JOINED": {
417
+ if (msg.iceServers) {
418
+ this.iceServers = msg.iceServers;
480
419
  }
481
- try {
482
- await pc.setRemoteDescription({
420
+ this.room.name = msg.room_name;
421
+ this.isWaitingForApproval = false;
422
+ this.pendingRequestId = null;
423
+ this.intentionalDisconnect = false;
424
+ this.reconnectAttempts = 0;
425
+ this.setupPublisherPC();
426
+ this.setupSubscriberPC();
427
+ await this.createPublisherOffer();
428
+ this.startHeartbeat();
429
+ this.joinResolver?.();
430
+ this.joinResolver = void 0;
431
+ this.joinRejecter = void 0;
432
+ break;
433
+ }
434
+ case "SFU_PUB_ANSWER": {
435
+ if (this.pubPC) {
436
+ await this.pubPC.setRemoteDescription({
483
437
  type: "answer",
484
438
  sdp: msg.payload
485
439
  });
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
440
  }
496
441
  break;
497
442
  }
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;
443
+ case "SFU_SUB_OFFER": {
444
+ if (this.subPC) {
445
+ await this.subPC.setRemoteDescription({
446
+ type: "offer",
447
+ sdp: msg.payload
448
+ });
449
+ const answer = await this.subPC.createAnswer();
450
+ await this.subPC.setLocalDescription(answer);
451
+ this.send({
452
+ type: "SUB_ANSWER",
453
+ payload: answer.sdp,
454
+ user_id: this.myId
455
+ });
505
456
  }
506
- if (!pc.remoteDescription) {
507
- (_c = this.pendingIceCandidates)[_d = msg.sender] ?? (_c[_d] = []);
508
- this.pendingIceCandidates[msg.sender].push(candidate);
509
- break;
457
+ break;
458
+ }
459
+ case "PUB_ICE": {
460
+ if (this.pubPC && msg.payload) {
461
+ await this.pubPC.addIceCandidate(JSON.parse(msg.payload)).catch(console.warn);
510
462
  }
511
- try {
512
- await pc.addIceCandidate(candidate);
513
- } catch (err) {
514
- console.warn("ICE error:", err);
463
+ break;
464
+ }
465
+ case "SUB_ICE": {
466
+ if (this.subPC && msg.payload) {
467
+ await this.subPC.addIceCandidate(JSON.parse(msg.payload)).catch(console.warn);
515
468
  }
516
469
  break;
517
470
  }
518
- case "EXISTING_USERS":
471
+ case "EXISTING_USERS": {
519
472
  if (msg.presenterId) {
520
473
  this.state.setPresenterId(msg.presenterId);
521
- this.events.onScreenShareStarted?.(msg.presenterId, null);
522
- this.state.setPresenterId(msg.presenterId);
523
474
  }
524
475
  for (const p of msg.participants || []) {
525
476
  if (!p?.id || p.id === this.myId) continue;
@@ -531,9 +482,6 @@ var VideoSDKCore = class {
531
482
  media: {
532
483
  stream: null,
533
484
  screenStream: void 0,
534
- cameraTrack: void 0,
535
- screenTrack: void 0,
536
- audioTrack: void 0,
537
485
  micEnabled: p.micEnabled ?? true,
538
486
  camEnabled: p.camEnabled ?? true,
539
487
  isScreenSharing: p.isScreenSharing ?? false,
@@ -543,120 +491,55 @@ var VideoSDKCore = class {
543
491
  };
544
492
  this.state.addParticipant(structuredParticipant);
545
493
  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
494
  }
558
495
  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
496
  }
598
497
  case "USER_JOINED": {
599
498
  const p = msg.participant;
600
499
  if (!p?.id || p.id === this.myId) return;
601
500
  this.state.addParticipant(p);
602
501
  this.events.onUserJoined?.(p);
603
- if (this.shouldInitiate(p.id)) {
604
- await this.createOffer(p.id);
605
- }
606
502
  break;
607
503
  }
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":
504
+ case "USER_LEFT": {
643
505
  const peerId = msg.participant.id;
644
- this.closePeer(peerId);
645
506
  this.state.removeParticipant(peerId);
646
507
  this.events.onUserLeft?.(peerId);
647
508
  break;
509
+ }
648
510
  case "MEDIA_STATE_CHANGE": {
649
- const peerId2 = msg.peerId;
511
+ const peerId = msg.peerId;
650
512
  const { kind, enabled } = msg;
651
513
  if (kind === "audio") {
652
- this.state.updateParticipantMedia(peerId2, { micEnabled: enabled });
653
- this.events.onMicToggled?.(peerId2, enabled);
514
+ this.state.updateParticipantMedia(peerId, { micEnabled: enabled });
515
+ this.events.onMicToggled?.(peerId, enabled);
654
516
  } else if (kind === "video") {
655
- this.state.updateParticipantMedia(peerId2, { camEnabled: enabled });
656
- this.events.onCamToggled?.(peerId2, enabled);
517
+ this.state.updateParticipantMedia(peerId, { camEnabled: enabled });
518
+ this.events.onCamToggled?.(peerId, enabled);
657
519
  }
658
520
  break;
659
521
  }
522
+ case "SCREEN_SHARE_START": {
523
+ const peerId = msg.peerId;
524
+ this.state.updateParticipantMedia(peerId, {
525
+ isScreenSharing: true,
526
+ remoteScreenStreamId: msg.stream_id,
527
+ cameraStreamId: msg?.camera_stream_id
528
+ });
529
+ if (!this.state.presenterId) {
530
+ this.state.setPresenterId(peerId);
531
+ }
532
+ break;
533
+ }
534
+ case "SCREEN_SHARE_STOP": {
535
+ const peerId = msg.peerId;
536
+ this.state.updateParticipantMedia(peerId, { isScreenSharing: false });
537
+ if (this.state.presenterId === peerId) {
538
+ this.state.setPresenterId(null);
539
+ }
540
+ this.events.onScreenShareStopped?.(peerId);
541
+ break;
542
+ }
660
543
  case "CHAT_MESSAGE": {
661
544
  const newMsg = msg.data;
662
545
  if (newMsg.sender_id === this.myId) break;
@@ -671,27 +554,37 @@ var VideoSDKCore = class {
671
554
  this.events.onChatMessage?.(msg);
672
555
  break;
673
556
  }
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
557
+ case "JOIN_PENDING": {
558
+ const req = msg.request;
559
+ this.isWaitingForApproval = true;
560
+ this.pendingRequestId = req.request_id;
561
+ this.events.onEntryRequested?.({
562
+ requestId: req.request_id,
563
+ userId: req.user_id,
564
+ name: req.name
680
565
  });
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
566
  break;
687
567
  }
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);
568
+ case "JOIN_APPROVED": {
569
+ this.isWaitingForApproval = false;
570
+ this.pendingRequestId = null;
571
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
572
+ this.send({
573
+ type: "JOIN",
574
+ room_id: this.room.id,
575
+ user_id: this.myId,
576
+ sender_name: this.participantName
577
+ });
693
578
  }
694
- this.events.onScreenShareStopped?.(peerId2);
579
+ break;
580
+ }
581
+ case "JOIN_REJECTED": {
582
+ this.isWaitingForApproval = false;
583
+ this.pendingRequestId = null;
584
+ this.events.onEntryResponded?.({
585
+ participantId: msg.user_id,
586
+ decision: "rejected"
587
+ });
695
588
  break;
696
589
  }
697
590
  case "ERROR": {
@@ -702,259 +595,78 @@ var VideoSDKCore = class {
702
595
  msg,
703
596
  !fatal
704
597
  );
705
- if (fatal) {
706
- this.disconnect();
707
- }
598
+ if (fatal) this.disconnect();
708
599
  return;
709
600
  }
710
601
  }
711
602
  }
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];
603
+ // ---------------- PUBLISHER RENEGOTIATION ----------------
604
+ async createPublisherOffer() {
605
+ if (!this.pubPC) return;
830
606
  try {
831
- const offer = await pc.createOffer();
832
- await pc.setLocalDescription(offer);
607
+ const offer = await this.pubPC.createOffer();
608
+ await this.pubPC.setLocalDescription(offer);
833
609
  this.send({
834
- type: "OFFER",
610
+ type: "PUB_OFFER",
835
611
  payload: offer.sdp,
836
- sender: this.myId,
837
- target: id
612
+ user_id: this.myId,
613
+ room_id: this.room.id
838
614
  });
839
- console.debug(`[Offer] Sent to ${id}`);
840
615
  } catch (err) {
841
- console.error(`[Offer] Failed for ${id}:`, err);
616
+ console.error("[SFU Publisher Offer Error]", err);
842
617
  }
843
618
  }
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
- }
619
+ // ---------------- MEDIA TOGGLES ----------------
620
+ toggleMic() {
621
+ const mediaState = this.state.localParticipant?.media;
622
+ if (!mediaState) return;
623
+ const nextEnabled = !mediaState.micEnabled;
624
+ this.localStream?.getAudioTracks().forEach((t) => t.enabled = nextEnabled);
625
+ this.state.updateLocalParticipant({
626
+ id: this.myId,
627
+ name: this.participantName,
628
+ media: { ...mediaState, micEnabled: nextEnabled }
629
+ });
630
+ this.send({ type: "MEDIA_STATE", kind: "audio", enabled: nextEnabled });
913
631
  }
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);
632
+ toggleCam() {
633
+ const mediaState = this.state.localParticipant?.media;
634
+ if (!mediaState) return;
635
+ const nextEnabled = !mediaState.camEnabled;
636
+ this.localStream?.getVideoTracks().forEach((t) => t.enabled = nextEnabled);
637
+ this.state.updateLocalParticipant({
638
+ id: this.myId,
639
+ name: this.participantName,
640
+ media: { ...mediaState, camEnabled: nextEnabled }
641
+ });
642
+ this.send({ type: "MEDIA_STATE", kind: "video", enabled: nextEnabled });
925
643
  }
644
+ // ---------------- SCREEN SHARING (SFU) ----------------
926
645
  async startScreenShare() {
927
646
  try {
928
647
  if (this.state.presenterId && this.state.presenterId !== this.myId) {
929
648
  throw new Error("Another user is already sharing their screen.");
930
649
  }
931
- if (!navigator.mediaDevices?.getDisplayMedia) {
932
- throw new Error("Screen sharing not supported on this device");
933
- }
934
650
  this.screenStream = await navigator.mediaDevices.getDisplayMedia({
935
651
  video: true
936
- // audio: true,
937
652
  });
938
653
  this.isScreenSharing = true;
654
+ const screenTrack = this.screenStream.getVideoTracks()[0];
655
+ if (this.pubPC) {
656
+ this.screenSender = this.pubPC.addTrack(screenTrack, this.screenStream);
657
+ await this.createPublisherOffer();
658
+ }
939
659
  this.state.updateLocalParticipant({
940
660
  media: {
941
661
  isScreenSharing: true,
942
662
  screenStream: this.screenStream,
943
- screenTrack: this.screenStream.getVideoTracks()[0]
663
+ screenTrack
944
664
  }
945
665
  });
946
666
  this.state.setPresenterId(this.myId);
947
- this.screenStream.getVideoTracks()[0].onended = () => {
667
+ screenTrack.onended = () => {
948
668
  this.stopScreenShare();
949
669
  };
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
670
  this.send({
959
671
  type: "SCREEN_SHARE_START",
960
672
  sender: this.myId,
@@ -966,7 +678,7 @@ var VideoSDKCore = class {
966
678
  } catch (err) {
967
679
  this.emitError(
968
680
  "SCREEN_SHARE_FAILED",
969
- err?.message || "Failed to start screen sharing",
681
+ err?.message || "Failed screen share",
970
682
  err,
971
683
  true
972
684
  );
@@ -978,18 +690,15 @@ var VideoSDKCore = class {
978
690
  stopScreenShare() {
979
691
  if (!this.screenStream) return;
980
692
  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
- });
693
+ if (this.pubPC && this.screenSender) {
694
+ try {
695
+ this.pubPC.removeTrack(this.screenSender);
696
+ this.createPublisherOffer();
697
+ } catch (e) {
698
+ console.warn("Failed removing screen sender", e);
699
+ }
700
+ this.screenSender = null;
701
+ }
993
702
  this.screenStream = null;
994
703
  this.isScreenSharing = false;
995
704
  this.state.updateLocalParticipant({
@@ -1008,15 +717,10 @@ var VideoSDKCore = class {
1008
717
  room_id: this.room.id
1009
718
  });
1010
719
  }
720
+ // ---------------- CHAT & RECONNECT ----------------
1011
721
  sendChatMessage(payload) {
1012
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1013
- console.warn("WS not connected");
722
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.room.id)
1014
723
  return;
1015
- }
1016
- if (!this.room.id) {
1017
- console.warn("No roomId set");
1018
- return;
1019
- }
1020
724
  const isPrivate = !!payload?.target;
1021
725
  const senderName = this.state.localParticipant?.name || "Anonymous";
1022
726
  const msg = {
@@ -1040,12 +744,48 @@ var VideoSDKCore = class {
1040
744
  client_ts: Date.now()
1041
745
  });
1042
746
  }
747
+ scheduleReconnect() {
748
+ if (!this.room.id) return;
749
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
750
+ window.clearTimeout(this.reconnectTimer);
751
+ this.reconnectTimer = window.setTimeout(async () => {
752
+ try {
753
+ await this.connect(this.room.id, this.participantName);
754
+ this.reconnectAttempts = 0;
755
+ } catch {
756
+ this.reconnectAttempts++;
757
+ this.scheduleReconnect();
758
+ }
759
+ }, delay);
760
+ }
761
+ startHeartbeat() {
762
+ this.stopHeartbeat();
763
+ this.pingInterval = setInterval(() => {
764
+ if (this.ws?.readyState === WebSocket.OPEN) {
765
+ this.send({ type: "PING", client_ts: Date.now() });
766
+ }
767
+ }, 2e4);
768
+ }
769
+ stopHeartbeat() {
770
+ if (this.pingInterval) {
771
+ clearInterval(this.pingInterval);
772
+ this.pingInterval = null;
773
+ }
774
+ }
775
+ reset() {
776
+ this.pubPC?.close();
777
+ this.subPC?.close();
778
+ this.pubPC = null;
779
+ this.subPC = null;
780
+ this.state.resetRemoteState();
781
+ }
1043
782
  disconnect() {
1044
783
  this.intentionalDisconnect = true;
1045
784
  this.stopScreenShare();
1046
- Object.values(this.peers).forEach((pc) => pc.close());
1047
- this.peers = {};
1048
- this.initiators.clear();
785
+ this.pubPC?.close();
786
+ this.subPC?.close();
787
+ this.pubPC = null;
788
+ this.subPC = null;
1049
789
  this.stopHeartbeat();
1050
790
  if (this.ws?.readyState === WebSocket.OPEN) {
1051
791
  this.send({
@@ -1072,32 +812,40 @@ var VideoSDKCore = class {
1072
812
  this.state.clearChat();
1073
813
  this.state.setPresenterId(null);
1074
814
  }
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
- }
815
+ async restartPublisherIce() {
816
+ if (!this.pubPC) return;
817
+ try {
818
+ this.pubPC.restartIce();
819
+ await this.createPublisherOffer();
820
+ } catch (err) {
821
+ console.error("[Publisher ICE Restart Failed]", err);
1084
822
  }
1085
- delete this.pendingIceCandidates[id];
823
+ }
824
+ emitError(code, message, raw, recoverable = true) {
825
+ const err = {
826
+ code,
827
+ message,
828
+ raw,
829
+ roomId: this.room.id,
830
+ userId: this.myId,
831
+ recoverable
832
+ };
833
+ this.events.onError?.(err);
834
+ this.joinRejecter?.(err);
835
+ this.joinRejecter = void 0;
836
+ console.error("[MeetingSDK Error]", err);
1086
837
  }
1087
838
  send(msg) {
1088
839
  this.ws?.send(JSON.stringify(msg));
1089
840
  }
1090
841
  approveJoinRequest(requestId) {
1091
- this.send({
1092
- type: "JOIN_APPROVE",
1093
- request_id: requestId
1094
- });
842
+ this.send({ type: "JOIN_APPROVE", request_id: requestId });
1095
843
  }
1096
844
  rejectJoinRequest(requestId) {
1097
- this.send({
1098
- type: "JOIN_REJECT",
1099
- request_id: requestId
1100
- });
845
+ this.send({ type: "JOIN_REJECT", request_id: requestId });
846
+ }
847
+ getMeeting() {
848
+ return this.room;
1101
849
  }
1102
850
  };
1103
851