@afosecure/meetingsdk 1.4.4 → 1.4.6

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