@defcomm/meetingsdk 0.1.1 → 0.1.3

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
@@ -387,6 +387,9 @@ var SFUClient = class {
387
387
  this.videoSender = null;
388
388
  this.audioSender = null;
389
389
  this.screenSender = null;
390
+ this.audioTransceiver = null;
391
+ this.cameraTransceiver = null;
392
+ this.screenTransceiver = null;
390
393
  this.iceServers = [];
391
394
  this.requestId = 0;
392
395
  this.publisherRevision = 0;
@@ -418,6 +421,7 @@ var SFUClient = class {
418
421
  this.trackMetadata = /* @__PURE__ */ new Map();
419
422
  this.participants = /* @__PURE__ */ new Map();
420
423
  this.remoteStreams = /* @__PURE__ */ new Map();
424
+ this.remoteScreenStreams = /* @__PURE__ */ new Map();
421
425
  this.trackToParticipant = /* @__PURE__ */ new Map();
422
426
  this.trackToSource = /* @__PURE__ */ new Map();
423
427
  this.localTrackIds = /* @__PURE__ */ new Map();
@@ -900,16 +904,18 @@ var SFUClient = class {
900
904
  }
901
905
  case "screen_share_stopped": {
902
906
  const participantId = payload.participant_id;
903
- const stream = participantId ? this.remoteStreams.get(participantId) : null;
907
+ const screenStream = participantId ? this.remoteScreenStreams.get(participantId) : null;
904
908
  const trackId = payload.track_id;
905
- if (stream) {
906
- for (const track of stream.getVideoTracks()) {
907
- const source = this.trackToSource.get(this.normalizeRemoteTrackId(track.id));
908
- if (!trackId || this.normalizeRemoteTrackId(track.id) === String(trackId) || source === "screen") {
909
- stream.removeTrack(track);
909
+ if (screenStream) {
910
+ for (const track of screenStream.getVideoTracks()) {
911
+ if (!trackId || this.normalizeRemoteTrackId(track.id) === String(trackId)) {
912
+ screenStream.removeTrack(track);
910
913
  track.stop();
911
914
  }
912
915
  }
916
+ if (screenStream.getTracks().length === 0 && participantId) {
917
+ this.remoteScreenStreams.delete(participantId);
918
+ }
913
919
  }
914
920
  this.onScreenShareStopped(participantId, trackId);
915
921
  break;
@@ -978,6 +984,18 @@ var SFUClient = class {
978
984
  const config = { iceServers: this.iceServers };
979
985
  this.publisher = new RTCPeerConnection(config);
980
986
  this.subscriber = new RTCPeerConnection(config);
987
+ this.audioTransceiver = this.publisher.addTransceiver("audio", {
988
+ direction: "sendonly"
989
+ });
990
+ this.cameraTransceiver = this.publisher.addTransceiver("video", {
991
+ direction: "sendonly"
992
+ });
993
+ this.screenTransceiver = this.publisher.addTransceiver("video", {
994
+ direction: "inactive"
995
+ });
996
+ this.audioSender = this.audioTransceiver.sender;
997
+ this.videoSender = this.cameraTransceiver.sender;
998
+ this.screenSender = this.screenTransceiver.sender;
981
999
  this.publisher.onicecandidate = (event) => {
982
1000
  if (event.candidate) this.sendCandidate("publisher", event.candidate);
983
1001
  };
@@ -1017,25 +1035,61 @@ var SFUClient = class {
1017
1035
  return;
1018
1036
  }
1019
1037
  const participantId = metadata.participant_id;
1038
+ const source = String(
1039
+ metadata.source || this.trackToSource.get(metadata.track_id) || ""
1040
+ ).trim().toLowerCase();
1041
+ if (source === "screen" || source === "screen_share") {
1042
+ let screenStream = this.remoteScreenStreams.get(participantId);
1043
+ if (!screenStream) {
1044
+ screenStream = new MediaStream();
1045
+ this.remoteScreenStreams.set(participantId, screenStream);
1046
+ }
1047
+ for (const existing of screenStream.getVideoTracks()) {
1048
+ if (existing.id !== event.track.id) {
1049
+ screenStream.removeTrack(existing);
1050
+ existing.stop();
1051
+ }
1052
+ }
1053
+ if (!screenStream.getTracks().some((track) => track.id === event.track.id)) {
1054
+ screenStream.addTrack(event.track);
1055
+ }
1056
+ event.track.onended = () => {
1057
+ try {
1058
+ screenStream?.removeTrack(event.track);
1059
+ } catch {
1060
+ }
1061
+ if (screenStream?.getTracks().length === 0) {
1062
+ this.remoteScreenStreams.delete(participantId);
1063
+ this.onScreenShareStopped(
1064
+ participantId,
1065
+ metadata.track_id
1066
+ );
1067
+ }
1068
+ };
1069
+ this.onScreenShareStarted(participantId, metadata);
1070
+ this.onRemoteTrack(event.track, screenStream, {
1071
+ ...metadata,
1072
+ source: "screen"
1073
+ });
1074
+ return;
1075
+ }
1076
+ if (event.track.kind === "video" && source !== "camera") {
1077
+ this.onError(
1078
+ new Error(
1079
+ `Refusing to route unknown remote video source to camera: ${metadata.track_id}`
1080
+ )
1081
+ );
1082
+ return;
1083
+ }
1020
1084
  let remoteStream = this.remoteStreams.get(participantId);
1021
1085
  if (!remoteStream) {
1022
1086
  remoteStream = new MediaStream();
1023
1087
  this.remoteStreams.set(participantId, remoteStream);
1024
1088
  }
1025
- const source = metadata.source || this.trackToSource.get(metadata.track_id);
1026
- if (source === "screen") {
1027
- for (const existing of remoteStream.getVideoTracks()) {
1028
- if (existing.id !== event.track.id && this.trackToSource.get(existing.id) === "screen") {
1029
- remoteStream.removeTrack(existing);
1030
- existing.stop();
1031
- }
1032
- }
1033
- } else {
1034
- for (const existing of remoteStream.getTracks()) {
1035
- if (existing.kind === event.track.kind && existing.id !== event.track.id) {
1036
- remoteStream.removeTrack(existing);
1037
- existing.stop();
1038
- }
1089
+ for (const existing of remoteStream.getTracks()) {
1090
+ if (existing.kind === event.track.kind && existing.id !== event.track.id) {
1091
+ remoteStream.removeTrack(existing);
1092
+ existing.stop();
1039
1093
  }
1040
1094
  }
1041
1095
  if (!remoteStream.getTracks().some((track) => track.id === event.track.id)) {
@@ -1047,31 +1101,24 @@ var SFUClient = class {
1047
1101
  } catch {
1048
1102
  }
1049
1103
  };
1050
- if (source === "screen") {
1051
- this.onScreenShareStarted(participantId, metadata);
1052
- }
1053
- this.onRemoteTrack(event.track, remoteStream, metadata);
1104
+ this.onRemoteTrack(event.track, remoteStream, {
1105
+ ...metadata,
1106
+ source: event.track.kind === "audio" ? "audio" : "camera"
1107
+ });
1054
1108
  }
1055
1109
  normalizeRemoteTrackId(browserTrackId) {
1056
1110
  return browserTrackId?.startsWith("track-") ? browserTrackId.slice("track-".length) : browserTrackId;
1057
1111
  }
1058
1112
  resolveRemoteTrackMetadata(track, stream) {
1059
- const sfuTrackId = this.normalizeRemoteTrackId(track.id);
1060
- const exact = this.trackMetadata.get(sfuTrackId);
1061
- if (exact) return exact;
1062
- const byTrackId = this.trackMetadata.get(track.id);
1063
- if (byTrackId) return byTrackId;
1064
- const matching = [...this.trackMetadata.values()].find(
1065
- (entry) => entry.kind === track.kind
1066
- );
1067
- if (matching) return matching;
1068
- if (stream?.id) {
1069
- const participant = this.participants.get(stream.id);
1070
- if (participant) {
1071
- return [...this.trackMetadata.values()].find(
1072
- (entry) => entry.participant_id === participant.participant_id && entry.kind === track.kind
1073
- ) || null;
1074
- }
1113
+ const candidates = [
1114
+ this.normalizeRemoteTrackId(track.id),
1115
+ track.id,
1116
+ stream?.id ? this.normalizeRemoteTrackId(stream.id) : null,
1117
+ stream?.id || null
1118
+ ].filter(Boolean);
1119
+ for (const id of candidates) {
1120
+ const metadata = this.trackMetadata.get(id);
1121
+ if (metadata) return metadata;
1075
1122
  }
1076
1123
  return null;
1077
1124
  }
@@ -1202,20 +1249,19 @@ var SFUClient = class {
1202
1249
  }
1203
1250
  async publishExistingTracks(audio = true, video = true) {
1204
1251
  if (!this.publisher || !this.localStream) return;
1205
- this.audioSender = null;
1206
- this.videoSender = null;
1207
- for (const track of this.localStream.getTracks()) {
1208
- if (track.kind === "audio" && !audio) continue;
1209
- if (track.kind === "video" && !video) continue;
1210
- const sender = this.publisher.addTrack(track, this.localStream);
1211
- if (track.kind === "audio") this.audioSender = sender;
1212
- if (track.kind === "video") this.videoSender = sender;
1213
- }
1214
- if (this.screenStream?.getVideoTracks()[0]) {
1215
- const screenTrack = this.screenStream.getVideoTracks()[0];
1216
- this.screenSender = this.publisher.addTrack(screenTrack, this.screenStream);
1217
- }
1218
- if (!this.publisher.getSenders().length) return;
1252
+ if (!this.audioTransceiver || !this.cameraTransceiver || !this.screenTransceiver) {
1253
+ throw new Error("Publisher transceivers are not initialized");
1254
+ }
1255
+ const audioTrack = this.localStream.getAudioTracks().find((track) => track.readyState === "live");
1256
+ const cameraTrack = this.localStream.getVideoTracks().find((track) => track.readyState === "live");
1257
+ const screenTrack = this.screenStream?.getVideoTracks().find((track) => track.readyState === "live");
1258
+ await this.audioSender?.replaceTrack(audio && audioTrack ? audioTrack : null);
1259
+ this.audioTransceiver.direction = audio && audioTrack ? "sendonly" : "inactive";
1260
+ await this.videoSender?.replaceTrack(video && cameraTrack ? cameraTrack : null);
1261
+ this.cameraTransceiver.direction = video && cameraTrack ? "sendonly" : "inactive";
1262
+ await this.screenSender?.replaceTrack(screenTrack || null);
1263
+ this.screenTransceiver.direction = screenTrack ? "sendonly" : "inactive";
1264
+ if (!audioTrack && !cameraTrack && !screenTrack) return;
1219
1265
  const offer = await this.publisher.createOffer();
1220
1266
  await this.publisher.setLocalDescription(offer);
1221
1267
  this.send({
@@ -1225,44 +1271,86 @@ var SFUClient = class {
1225
1271
  sdp: offer.sdp
1226
1272
  }
1227
1273
  });
1228
- if (this.screenStream?.getVideoTracks()[0]) {
1274
+ if (screenTrack) {
1229
1275
  this.send({ type: "start_screen_share" });
1230
1276
  }
1231
1277
  }
1232
1278
  async shareScreen() {
1233
- if (!this.publisher) throw new Error("Publisher PeerConnection is not initialized");
1234
- if (this.screenStream) return this.screenStream;
1235
- this.screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
1236
- const screenTrack = this.screenStream.getVideoTracks()[0];
1237
- if (!screenTrack) throw new Error("Screen share returned no video track");
1238
- this.screenSender = this.publisher.addTrack(screenTrack, this.screenStream);
1239
- const offer = await this.publisher.createOffer();
1240
- await this.publisher.setLocalDescription(offer);
1241
- this.send({
1242
- type: "publish_offer",
1243
- payload: {
1244
- revision: ++this.publisherRevision,
1245
- sdp: offer.sdp
1246
- }
1279
+ if (!this.publisher) {
1280
+ throw new Error("Publisher PeerConnection is not initialized");
1281
+ }
1282
+ if (!this.screenTransceiver || !this.screenSender) {
1283
+ throw new Error("Screen transceiver is not initialized");
1284
+ }
1285
+ if (this.screenStream) {
1286
+ return this.screenStream;
1287
+ }
1288
+ const screenStream = await navigator.mediaDevices.getDisplayMedia({
1289
+ video: true,
1290
+ audio: false
1247
1291
  });
1248
- this.send({ type: "start_screen_share" });
1249
- screenTrack.onended = () => {
1250
- void this.stopScreenShare();
1251
- };
1252
- return this.screenStream;
1253
- }
1254
- async stopScreenShare() {
1255
- if (!this.screenStream) return;
1256
- const screenTrack = this.screenStream.getVideoTracks()[0];
1257
- if (screenTrack) screenTrack.onended = null;
1258
- if (this.screenSender && this.publisher) {
1292
+ const screenTrack = screenStream.getVideoTracks()[0];
1293
+ if (!screenTrack) {
1294
+ screenStream.getTracks().forEach((track) => track.stop());
1295
+ throw new Error("Screen share returned no video track");
1296
+ }
1297
+ this.screenStream = screenStream;
1298
+ try {
1299
+ if (!this.send({ type: "start_screen_share" })) {
1300
+ throw new Error("Failed to signal start_screen_share");
1301
+ }
1302
+ await this.screenSender.replaceTrack(screenTrack);
1303
+ this.screenTransceiver.direction = "sendonly";
1304
+ const offer = await this.publisher.createOffer();
1305
+ await this.publisher.setLocalDescription(offer);
1306
+ this.send({
1307
+ type: "publish_offer",
1308
+ payload: {
1309
+ revision: ++this.publisherRevision,
1310
+ sdp: offer.sdp
1311
+ }
1312
+ });
1313
+ screenTrack.onended = () => {
1314
+ void this.stopScreenShare();
1315
+ };
1316
+ console.info("[publisher] Screen track attached", {
1317
+ screenTrackId: screenTrack.id,
1318
+ cameraTrackId: this.videoSender?.track?.id,
1319
+ screenMid: this.screenTransceiver.mid,
1320
+ cameraMid: this.cameraTransceiver?.mid
1321
+ });
1322
+ return this.screenStream;
1323
+ } catch (error) {
1259
1324
  try {
1260
- this.publisher.removeTrack(this.screenSender);
1325
+ await this.screenSender.replaceTrack(null);
1261
1326
  } catch {
1262
1327
  }
1263
- this.screenSender = null;
1328
+ this.screenTransceiver.direction = "inactive";
1329
+ screenStream.getTracks().forEach((track) => track.stop());
1330
+ this.screenStream = null;
1264
1331
  try {
1265
- const offer = await this.publisher.createOffer();
1332
+ this.send({ type: "stop_screen_share" });
1333
+ } catch {
1334
+ }
1335
+ throw error;
1336
+ }
1337
+ }
1338
+ async stopScreenShare() {
1339
+ if (!this.screenStream && !this.screenSender?.track) return;
1340
+ const screenTrack = this.screenSender?.track || null;
1341
+ if (screenTrack) screenTrack.onended = null;
1342
+ try {
1343
+ await this.screenSender?.replaceTrack(null);
1344
+ } catch (error) {
1345
+ this.onError(error);
1346
+ }
1347
+ if (this.screenTransceiver) {
1348
+ this.screenTransceiver.direction = "inactive";
1349
+ }
1350
+ this.send({ type: "stop_screen_share" });
1351
+ try {
1352
+ const offer = await this.publisher?.createOffer();
1353
+ if (offer && this.publisher) {
1266
1354
  await this.publisher.setLocalDescription(offer);
1267
1355
  this.send({
1268
1356
  type: "publish_offer",
@@ -1271,13 +1359,15 @@ var SFUClient = class {
1271
1359
  sdp: offer.sdp
1272
1360
  }
1273
1361
  });
1274
- this.send({ type: "stop_screen_share" });
1275
- } catch (error) {
1276
- this.onError(error);
1277
1362
  }
1363
+ } catch (error) {
1364
+ this.onError(error);
1278
1365
  }
1279
- this.screenStream.getTracks().forEach((track) => track.stop());
1366
+ this.screenStream?.getTracks().forEach((track) => track.stop());
1280
1367
  this.screenStream = null;
1368
+ if (this.currentParticipantId) {
1369
+ this.onScreenShareStopped(this.currentParticipantId);
1370
+ }
1281
1371
  }
1282
1372
  muteAudio(muted) {
1283
1373
  this.localStream?.getAudioTracks().forEach((track) => {
@@ -1363,6 +1453,11 @@ var SFUClient = class {
1363
1453
  stream.getTracks().forEach((track) => track.stop());
1364
1454
  this.remoteStreams.delete(participantId);
1365
1455
  }
1456
+ const screenStream = this.remoteScreenStreams.get(participantId);
1457
+ if (screenStream) {
1458
+ screenStream.getTracks().forEach((track) => track.stop());
1459
+ this.remoteScreenStreams.delete(participantId);
1460
+ }
1366
1461
  for (const [trackId, metadata] of this.trackMetadata) {
1367
1462
  if (metadata.participant_id === participantId) {
1368
1463
  this.trackMetadata.delete(trackId);
@@ -1438,6 +1533,9 @@ var SFUClient = class {
1438
1533
  this.videoSender = null;
1439
1534
  this.audioSender = null;
1440
1535
  this.screenSender = null;
1536
+ this.audioTransceiver = null;
1537
+ this.cameraTransceiver = null;
1538
+ this.screenTransceiver = null;
1441
1539
  this.pendingIce = { publisher: [], subscriber: [] };
1442
1540
  this.offerQueue = [];
1443
1541
  this.subscriberNegotiating = false;
@@ -1460,6 +1558,10 @@ var SFUClient = class {
1460
1558
  stream.getTracks().forEach((track) => track.stop());
1461
1559
  }
1462
1560
  this.remoteStreams.clear();
1561
+ for (const stream of this.remoteScreenStreams.values()) {
1562
+ stream.getTracks().forEach((track) => track.stop());
1563
+ }
1564
+ this.remoteScreenStreams.clear();
1463
1565
  this.trackMetadata.clear();
1464
1566
  this.trackToParticipant.clear();
1465
1567
  this.trackToSource.clear();
@@ -1500,7 +1602,7 @@ var SFUClient = class {
1500
1602
 
1501
1603
  // src/core/VideoCore.ts
1502
1604
  var VideoSDKCore = class {
1503
- constructor(events = {}, apiBase = SDK_CONFIG.apiBase || "https://api.example.com/api", wsBase = SDK_CONFIG.wsBase || "wss://api.example.com/ws") {
1605
+ constructor(events = {}, apiBase = SDK_CONFIG.apiBase, wsBase = SDK_CONFIG.wsBase) {
1504
1606
  this.events = events;
1505
1607
  this.apiBase = apiBase;
1506
1608
  this.wsBase = wsBase;
@@ -1516,6 +1618,8 @@ var VideoSDKCore = class {
1516
1618
  this.intentionalDisconnect = false;
1517
1619
  this.remoteStreams = /* @__PURE__ */ new Map();
1518
1620
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
1621
+ this.joinPromise = null;
1622
+ this.disconnectPromise = null;
1519
1623
  this.state = new MeetingState();
1520
1624
  this.events = events;
1521
1625
  this.activeSpeakerDetector = new ActiveSpeakerDetector({
@@ -1549,7 +1653,7 @@ var VideoSDKCore = class {
1549
1653
  this.events.onError?.(err);
1550
1654
  console.error("[MeetingSDK Error]", err);
1551
1655
  }
1552
- // ============ LOCAL MEDIA ============
1656
+ // LOCAL MEDIA
1553
1657
  async initLocal(video, name) {
1554
1658
  this.participantName = name;
1555
1659
  try {
@@ -1598,7 +1702,7 @@ var VideoSDKCore = class {
1598
1702
  });
1599
1703
  this.state.localStream = this.localStream;
1600
1704
  }
1601
- // ============ CONNECTION / SIGNALLING ============
1705
+ // ROOM MANAGEMENT
1602
1706
  async createRoom(input) {
1603
1707
  const client = this.sfuClient ?? new SFUClient({
1604
1708
  apiBase: this.apiBase,
@@ -1623,6 +1727,7 @@ var VideoSDKCore = class {
1623
1727
  });
1624
1728
  return client.deleteRoom(roomCode);
1625
1729
  }
1730
+ // JOIN
1626
1731
  async joinMeeting(config) {
1627
1732
  const {
1628
1733
  roomId,
@@ -1637,6 +1742,28 @@ var VideoSDKCore = class {
1637
1742
  if (userId) {
1638
1743
  this.myId = userId;
1639
1744
  }
1745
+ if (this.sfuClient && this.room.id === roomId && this.sfuClient.getConnectionState() === "connected") {
1746
+ console.info("[SDK] Already connected; duplicate join ignored", {
1747
+ roomId,
1748
+ userId: this.myId
1749
+ });
1750
+ return;
1751
+ }
1752
+ if (this.joinPromise) {
1753
+ console.info(
1754
+ "[SDK] Join already in progress; waiting for existing join",
1755
+ {
1756
+ roomId,
1757
+ userId: this.myId
1758
+ }
1759
+ );
1760
+ return this.joinPromise;
1761
+ }
1762
+ if (this.sfuClient && this.room.id && this.room.id !== roomId) {
1763
+ throw new Error(
1764
+ `Already connected to room ${this.room.id}. Leave the current meeting before joining another room.`
1765
+ );
1766
+ }
1640
1767
  this.intentionalDisconnect = false;
1641
1768
  this.participantName = name;
1642
1769
  this.room.id = roomId;
@@ -1658,19 +1785,43 @@ var VideoSDKCore = class {
1658
1785
  track.enabled = !videoMuted;
1659
1786
  });
1660
1787
  this.updateLocalMedia(audioMuted, videoMuted);
1661
- return this.connect(roomId, name);
1788
+ const promise = this.connect(roomId, name);
1789
+ this.joinPromise = promise;
1790
+ try {
1791
+ await promise;
1792
+ } finally {
1793
+ if (this.joinPromise === promise) {
1794
+ this.joinPromise = null;
1795
+ }
1796
+ }
1662
1797
  }
1798
+ // SFU CONNECTION
1663
1799
  async connect(roomId, name) {
1664
1800
  if (this.sfuClient) {
1665
- await this.sfuClient.leave();
1666
- this.sfuClient = null;
1801
+ const state = this.sfuClient.getConnectionState();
1802
+ if (this.room.id === roomId && (state === "connecting" || state === "connected" || state === "reconnecting")) {
1803
+ console.info(
1804
+ "[SDK] Existing SFU connection is active; connect ignored",
1805
+ {
1806
+ roomId,
1807
+ state
1808
+ }
1809
+ );
1810
+ return;
1811
+ }
1812
+ throw new Error(
1813
+ `SFU client already exists in state "${state}". Disconnect explicitly before creating another connection.`
1814
+ );
1667
1815
  }
1668
1816
  const client = new SFUClient({
1669
1817
  apiBase: this.apiBase,
1670
1818
  wsBase: this.wsBase,
1671
1819
  userId: this.myId,
1672
1820
  autoSubscribe: true,
1673
- onStateChange: (state) => this.handleSFUState(state),
1821
+ // CONNECTION STATE
1822
+ onStateChange: (state) => {
1823
+ this.handleSFUState(state);
1824
+ },
1674
1825
  onConnected: (payload, resumed) => {
1675
1826
  console.log(`[SFU] ${resumed ? "Resumed" : "Joined"} room`, payload);
1676
1827
  this.state.updateLocalParticipant({
@@ -1684,6 +1835,7 @@ var VideoSDKCore = class {
1684
1835
  onReconnecting: (attempt) => {
1685
1836
  console.warn(`[SFU] Reconnecting, attempt ${attempt}`);
1686
1837
  },
1838
+ // ADMISSION CONTROL
1687
1839
  onJoinPending: (info) => {
1688
1840
  console.log("[SFU] Join request pending", info);
1689
1841
  this.events.onEntryRequested?.({
@@ -1696,7 +1848,10 @@ var VideoSDKCore = class {
1696
1848
  console.log("[SFU] Join approved", requestId);
1697
1849
  this.events.onJoinApproved?.(requestId);
1698
1850
  this.events.onEntryResponded?.(
1699
- { participantId: this.myId, decision: "approved" },
1851
+ {
1852
+ participantId: this.myId,
1853
+ decision: "approved"
1854
+ },
1700
1855
  "approved"
1701
1856
  );
1702
1857
  },
@@ -1704,13 +1859,19 @@ var VideoSDKCore = class {
1704
1859
  console.warn("[SFU] Join rejected", requestId, reason);
1705
1860
  this.events.onJoinRejected?.(requestId);
1706
1861
  this.events.onEntryResponded?.(
1707
- { participantId: this.myId, decision: "rejected" },
1862
+ {
1863
+ participantId: this.myId,
1864
+ decision: "rejected"
1865
+ },
1708
1866
  "rejected"
1709
1867
  );
1710
1868
  this.emitError(
1711
1869
  "JOIN_REJECTED",
1712
1870
  reason || "Your request to join was rejected",
1713
- { requestId, reason },
1871
+ {
1872
+ requestId,
1873
+ reason
1874
+ },
1714
1875
  false
1715
1876
  );
1716
1877
  },
@@ -1718,16 +1879,21 @@ var VideoSDKCore = class {
1718
1879
  const requestId = request?.request_id || request?.requestId;
1719
1880
  const userId = request?.user_id || request?.userId;
1720
1881
  const nameValue = request?.user_metadata?.name || request?.display_name || request?.name || "Participant";
1721
- if (!requestId || !userId) return;
1882
+ if (!requestId || !userId) {
1883
+ return;
1884
+ }
1722
1885
  this.events.onEntryRequested?.({
1723
1886
  requestId,
1724
1887
  userId,
1725
1888
  name: nameValue
1726
1889
  });
1727
1890
  },
1891
+ // PARTICIPANTS
1728
1892
  onParticipantJoined: (participant) => {
1729
1893
  const pid = participant.participant_id;
1730
- if (!pid || pid === this.myId) return;
1894
+ if (!pid || pid === this.myId) {
1895
+ return;
1896
+ }
1731
1897
  const pname = participant.user_metadata?.name || participant.display_name || "Participant";
1732
1898
  this.state.addParticipant({
1733
1899
  id: pid,
@@ -1746,9 +1912,13 @@ var VideoSDKCore = class {
1746
1912
  });
1747
1913
  },
1748
1914
  onParticipantPresence: (participantId, presence) => {
1749
- if (participantId === this.myId) return;
1915
+ if (participantId === this.myId) {
1916
+ return;
1917
+ }
1750
1918
  const current = this.state.getParticipant(participantId);
1751
- if (!current) return;
1919
+ if (!current) {
1920
+ return;
1921
+ }
1752
1922
  const connectionState = presence === "reconnecting" ? "reconnecting" : presence === "connected" || presence === "online" ? "connected" : presence === "disconnected" ? "disconnected" : current.connectionState;
1753
1923
  this.state.updateParticipantMedia(participantId, {
1754
1924
  stream: current.media?.stream ?? null
@@ -1764,19 +1934,24 @@ var VideoSDKCore = class {
1764
1934
  },
1765
1935
  onParticipantLeft: (participant) => {
1766
1936
  const pid = typeof participant === "string" ? participant : participant.participant_id;
1767
- if (!pid) return;
1937
+ if (!pid) {
1938
+ return;
1939
+ }
1768
1940
  console.log("[SFU] Participant permanently left", pid);
1769
1941
  this.cleanupRemoteParticipant(pid);
1770
1942
  this.state.removeParticipant(pid);
1771
1943
  this.events.onUserLeft?.(pid);
1772
1944
  },
1945
+ // TRACKS
1773
1946
  onTrackPublished: (track) => {
1774
1947
  console.log("[SFU] Track published", track);
1775
1948
  },
1776
1949
  onTrackUnpublished: (track) => {
1777
1950
  console.log("[SFU] Track unpublished", track);
1778
1951
  const pid = track.participant_id;
1779
- if (!pid) return;
1952
+ if (!pid) {
1953
+ return;
1954
+ }
1780
1955
  const current = this.state.getParticipant(pid);
1781
1956
  if (track.source === "screen") {
1782
1957
  const screen = this.remoteScreenStreams.get(pid);
@@ -1790,7 +1965,9 @@ var VideoSDKCore = class {
1790
1965
  this.events.onScreenShareStopped?.(pid);
1791
1966
  return;
1792
1967
  }
1793
- if (!current) return;
1968
+ if (!current) {
1969
+ return;
1970
+ }
1794
1971
  this.state.updateParticipantMedia(pid, {
1795
1972
  stream: current.media?.stream ?? null,
1796
1973
  micEnabled: track.kind === "audio" ? false : current.media?.micEnabled ?? true,
@@ -1799,9 +1976,13 @@ var VideoSDKCore = class {
1799
1976
  },
1800
1977
  onTrackStateChanged: (track) => {
1801
1978
  const pid = track.participant_id;
1802
- if (!pid || pid === this.myId) return;
1979
+ if (!pid || pid === this.myId) {
1980
+ return;
1981
+ }
1803
1982
  const current = this.state.getParticipant(pid);
1804
- if (!current) return;
1983
+ if (!current) {
1984
+ return;
1985
+ }
1805
1986
  if (track.source === "audio") {
1806
1987
  this.state.updateParticipantMedia(pid, {
1807
1988
  stream: current.media?.stream ?? null,
@@ -1817,17 +1998,22 @@ var VideoSDKCore = class {
1817
1998
  }
1818
1999
  this.events.onTrackStateChanged?.(track);
1819
2000
  },
2001
+ // RECORDING
1820
2002
  onRecordingStarted: (recording) => {
1821
2003
  this.events.onRecordingStarted?.(recording);
1822
2004
  },
1823
2005
  onRecordingStopped: (recording) => {
1824
2006
  this.events.onRecordingStopped?.(recording);
1825
2007
  },
2008
+ // REMOTE MEDIA
1826
2009
  onRemoteTrack: (track, stream, metadata) => {
1827
2010
  this.handleRemoteTrack(track, stream, metadata);
1828
2011
  },
2012
+ // SCREEN SHARE
1829
2013
  onScreenShareStarted: (participantId, track) => {
1830
- if (participantId === this.myId) return;
2014
+ if (participantId === this.myId) {
2015
+ return;
2016
+ }
1831
2017
  console.log("[SFU] Screen share started", participantId, track);
1832
2018
  const current = this.state.getParticipant(participantId);
1833
2019
  if (current) {
@@ -1838,7 +2024,9 @@ var VideoSDKCore = class {
1838
2024
  }
1839
2025
  },
1840
2026
  onScreenShareStopped: (participantId) => {
1841
- if (participantId === this.myId) return;
2027
+ if (participantId === this.myId) {
2028
+ return;
2029
+ }
1842
2030
  const screen = this.remoteScreenStreams.get(participantId);
1843
2031
  screen?.getTracks().forEach((track) => track.stop());
1844
2032
  this.remoteScreenStreams.delete(participantId);
@@ -1849,6 +2037,7 @@ var VideoSDKCore = class {
1849
2037
  });
1850
2038
  this.events.onScreenShareStopped?.(participantId);
1851
2039
  },
2040
+ // ERROR
1852
2041
  onError: (error) => {
1853
2042
  console.error("[SFU] Error:", error);
1854
2043
  this.emitError(
@@ -1861,11 +2050,12 @@ var VideoSDKCore = class {
1861
2050
  });
1862
2051
  this.sfuClient = client;
1863
2052
  client.setLocalStream(this.localStream);
1864
- return client.getToken(roomId, {
2053
+ await client.getToken(roomId, {
1865
2054
  displayName: name,
1866
2055
  userId: this.myId
1867
2056
  });
1868
2057
  }
2058
+ // SFU STATE
1869
2059
  handleSFUState(state) {
1870
2060
  this.events.onConnectionStateChanged?.(state);
1871
2061
  if (state === "reconnecting") {
@@ -1884,9 +2074,12 @@ var VideoSDKCore = class {
1884
2074
  );
1885
2075
  }
1886
2076
  }
2077
+ // REMOTE TRACKS
1887
2078
  handleRemoteTrack(track, stream, metadata) {
1888
2079
  const participantId = metadata?.participant_id;
1889
- if (!participantId || participantId === this.myId) return;
2080
+ if (!participantId || participantId === this.myId) {
2081
+ return;
2082
+ }
1890
2083
  const source = metadata.source;
1891
2084
  if (source === "screen") {
1892
2085
  let screenStream = this.remoteScreenStreams.get(participantId);
@@ -1937,36 +2130,41 @@ var VideoSDKCore = class {
1937
2130
  this.events.onTrack?.(remoteStream, participantId);
1938
2131
  track.onended = () => {
1939
2132
  if (track.kind === "video") {
1940
- const participant2 = this.state.getParticipant(participantId);
1941
2133
  this.state.updateParticipantMedia(participantId, {
1942
2134
  camEnabled: false,
1943
2135
  stream: remoteStream
1944
2136
  });
1945
2137
  return;
1946
2138
  }
1947
- const participant = this.state.getParticipant(participantId);
1948
2139
  this.state.updateParticipantMedia(participantId, {
1949
2140
  micEnabled: false,
1950
2141
  stream: remoteStream
1951
2142
  });
1952
2143
  };
1953
2144
  }
2145
+ // MEETING
1954
2146
  getMeeting() {
1955
2147
  return this.room;
1956
2148
  }
1957
- // ============ ADMISSION CONTROL ============
2149
+ // ADMISSION CONTROL
1958
2150
  async approveJoinRequest(requestId) {
1959
- if (!this.sfuClient) throw new Error("SFU client is not connected");
2151
+ if (!this.sfuClient) {
2152
+ throw new Error("SFU client is not connected");
2153
+ }
1960
2154
  return this.sfuClient.approveJoinRequest(requestId);
1961
2155
  }
1962
2156
  async rejectJoinRequest(requestId, reason) {
1963
- if (!this.sfuClient) throw new Error("SFU client is not connected");
2157
+ if (!this.sfuClient) {
2158
+ throw new Error("SFU client is not connected");
2159
+ }
1964
2160
  return this.sfuClient.rejectJoinRequest(requestId, reason);
1965
2161
  }
1966
- // ============ MEDIA CONTROL ============
2162
+ // MEDIA CONTROL
1967
2163
  toggleMic() {
1968
2164
  const mediaState = this.state.localParticipant?.media;
1969
- if (!mediaState || !this.localStream) return;
2165
+ if (!mediaState || !this.localStream) {
2166
+ return;
2167
+ }
1970
2168
  const nextEnabled = !mediaState.micEnabled;
1971
2169
  this.localStream.getAudioTracks().forEach((track) => {
1972
2170
  track.enabled = nextEnabled;
@@ -1974,14 +2172,18 @@ var VideoSDKCore = class {
1974
2172
  this.state.updateLocalParticipant({
1975
2173
  id: this.myId,
1976
2174
  name: this.participantName,
1977
- media: { micEnabled: nextEnabled }
2175
+ media: {
2176
+ micEnabled: nextEnabled
2177
+ }
1978
2178
  });
1979
2179
  this.sfuClient?.muteAudio(!nextEnabled);
1980
2180
  this.events.onMicToggled?.(this.myId, nextEnabled);
1981
2181
  }
1982
2182
  toggleCam() {
1983
2183
  const mediaState = this.state.localParticipant?.media;
1984
- if (!mediaState || !this.localStream) return;
2184
+ if (!mediaState || !this.localStream) {
2185
+ return;
2186
+ }
1985
2187
  const nextEnabled = !mediaState.camEnabled;
1986
2188
  this.localStream.getVideoTracks().forEach((track) => {
1987
2189
  track.enabled = nextEnabled;
@@ -1989,12 +2191,14 @@ var VideoSDKCore = class {
1989
2191
  this.state.updateLocalParticipant({
1990
2192
  id: this.myId,
1991
2193
  name: this.participantName,
1992
- media: { camEnabled: nextEnabled }
2194
+ media: {
2195
+ camEnabled: nextEnabled
2196
+ }
1993
2197
  });
1994
2198
  this.sfuClient?.muteVideo(!nextEnabled);
1995
2199
  this.events.onCamToggled?.(this.myId, nextEnabled);
1996
2200
  }
1997
- // ============ SCREEN SHARING ============
2201
+ // SCREEN SHARING
1998
2202
  async startScreenShare() {
1999
2203
  try {
2000
2204
  if (!navigator.mediaDevices?.getDisplayMedia) {
@@ -2026,7 +2230,9 @@ var VideoSDKCore = class {
2026
2230
  }
2027
2231
  }
2028
2232
  stopScreenShare() {
2029
- if (!this.screenStream) return;
2233
+ if (!this.screenStream) {
2234
+ return;
2235
+ }
2030
2236
  void this.sfuClient?.stopScreenShare();
2031
2237
  this.screenStream.getTracks().forEach((track) => track.stop());
2032
2238
  this.screenStream = null;
@@ -2039,7 +2245,7 @@ var VideoSDKCore = class {
2039
2245
  }
2040
2246
  });
2041
2247
  }
2042
- // ============ RECORDING ============
2248
+ // RECORDING
2043
2249
  async startRecording() {
2044
2250
  if (!this.sfuClient) {
2045
2251
  throw new Error("SFU client is not connected");
@@ -2058,7 +2264,7 @@ var VideoSDKCore = class {
2058
2264
  }
2059
2265
  return this.sfuClient.getRecordingStatus();
2060
2266
  }
2061
- // ============ CHAT ============
2267
+ // CHAT
2062
2268
  sendChatMessage(payload) {
2063
2269
  if (!this.sfuClient) {
2064
2270
  console.warn("SFU Client not connected");
@@ -2077,7 +2283,7 @@ var VideoSDKCore = class {
2077
2283
  this.state.addChatMessage(msg);
2078
2284
  this.events.onChatMessage?.(msg);
2079
2285
  }
2080
- // ============ CLEANUP ============
2286
+ // CLEANUP
2081
2287
  cleanupRemoteParticipant(participantId) {
2082
2288
  this.activeSpeakerDetector.removeParticipant(participantId);
2083
2289
  const stream = this.remoteStreams.get(participantId);
@@ -2087,16 +2293,34 @@ var VideoSDKCore = class {
2087
2293
  screen?.getTracks().forEach((track) => track.stop());
2088
2294
  this.remoteScreenStreams.delete(participantId);
2089
2295
  }
2296
+ // DISCONNECT
2090
2297
  async disconnect() {
2298
+ if (this.disconnectPromise) {
2299
+ return this.disconnectPromise;
2300
+ }
2301
+ this.disconnectPromise = this.performDisconnect();
2302
+ try {
2303
+ await this.disconnectPromise;
2304
+ } finally {
2305
+ this.disconnectPromise = null;
2306
+ }
2307
+ }
2308
+ async performDisconnect() {
2091
2309
  this.intentionalDisconnect = true;
2092
- await this.sfuClient?.leave();
2310
+ this.joinPromise = null;
2311
+ const client = this.sfuClient;
2093
2312
  this.sfuClient = null;
2313
+ if (client) {
2314
+ await client.leave();
2315
+ }
2094
2316
  if (this.localStream) {
2095
2317
  this.localStream.getTracks().forEach((track) => track.stop());
2096
2318
  this.localStream = null;
2097
2319
  }
2098
- this.screenStream?.getTracks().forEach((track) => track.stop());
2099
- this.screenStream = null;
2320
+ if (this.screenStream) {
2321
+ this.screenStream.getTracks().forEach((track) => track.stop());
2322
+ this.screenStream = null;
2323
+ }
2100
2324
  this.isScreenSharing = false;
2101
2325
  for (const stream of this.remoteStreams.values()) {
2102
2326
  stream.getTracks().forEach((track) => track.stop());