@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.mjs CHANGED
@@ -351,6 +351,9 @@ var SFUClient = class {
351
351
  this.videoSender = null;
352
352
  this.audioSender = null;
353
353
  this.screenSender = null;
354
+ this.audioTransceiver = null;
355
+ this.cameraTransceiver = null;
356
+ this.screenTransceiver = null;
354
357
  this.iceServers = [];
355
358
  this.requestId = 0;
356
359
  this.publisherRevision = 0;
@@ -382,6 +385,7 @@ var SFUClient = class {
382
385
  this.trackMetadata = /* @__PURE__ */ new Map();
383
386
  this.participants = /* @__PURE__ */ new Map();
384
387
  this.remoteStreams = /* @__PURE__ */ new Map();
388
+ this.remoteScreenStreams = /* @__PURE__ */ new Map();
385
389
  this.trackToParticipant = /* @__PURE__ */ new Map();
386
390
  this.trackToSource = /* @__PURE__ */ new Map();
387
391
  this.localTrackIds = /* @__PURE__ */ new Map();
@@ -864,16 +868,18 @@ var SFUClient = class {
864
868
  }
865
869
  case "screen_share_stopped": {
866
870
  const participantId = payload.participant_id;
867
- const stream = participantId ? this.remoteStreams.get(participantId) : null;
871
+ const screenStream = participantId ? this.remoteScreenStreams.get(participantId) : null;
868
872
  const trackId = payload.track_id;
869
- if (stream) {
870
- for (const track of stream.getVideoTracks()) {
871
- const source = this.trackToSource.get(this.normalizeRemoteTrackId(track.id));
872
- if (!trackId || this.normalizeRemoteTrackId(track.id) === String(trackId) || source === "screen") {
873
- stream.removeTrack(track);
873
+ if (screenStream) {
874
+ for (const track of screenStream.getVideoTracks()) {
875
+ if (!trackId || this.normalizeRemoteTrackId(track.id) === String(trackId)) {
876
+ screenStream.removeTrack(track);
874
877
  track.stop();
875
878
  }
876
879
  }
880
+ if (screenStream.getTracks().length === 0 && participantId) {
881
+ this.remoteScreenStreams.delete(participantId);
882
+ }
877
883
  }
878
884
  this.onScreenShareStopped(participantId, trackId);
879
885
  break;
@@ -942,6 +948,18 @@ var SFUClient = class {
942
948
  const config = { iceServers: this.iceServers };
943
949
  this.publisher = new RTCPeerConnection(config);
944
950
  this.subscriber = new RTCPeerConnection(config);
951
+ this.audioTransceiver = this.publisher.addTransceiver("audio", {
952
+ direction: "sendonly"
953
+ });
954
+ this.cameraTransceiver = this.publisher.addTransceiver("video", {
955
+ direction: "sendonly"
956
+ });
957
+ this.screenTransceiver = this.publisher.addTransceiver("video", {
958
+ direction: "inactive"
959
+ });
960
+ this.audioSender = this.audioTransceiver.sender;
961
+ this.videoSender = this.cameraTransceiver.sender;
962
+ this.screenSender = this.screenTransceiver.sender;
945
963
  this.publisher.onicecandidate = (event) => {
946
964
  if (event.candidate) this.sendCandidate("publisher", event.candidate);
947
965
  };
@@ -981,25 +999,61 @@ var SFUClient = class {
981
999
  return;
982
1000
  }
983
1001
  const participantId = metadata.participant_id;
1002
+ const source = String(
1003
+ metadata.source || this.trackToSource.get(metadata.track_id) || ""
1004
+ ).trim().toLowerCase();
1005
+ if (source === "screen" || source === "screen_share") {
1006
+ let screenStream = this.remoteScreenStreams.get(participantId);
1007
+ if (!screenStream) {
1008
+ screenStream = new MediaStream();
1009
+ this.remoteScreenStreams.set(participantId, screenStream);
1010
+ }
1011
+ for (const existing of screenStream.getVideoTracks()) {
1012
+ if (existing.id !== event.track.id) {
1013
+ screenStream.removeTrack(existing);
1014
+ existing.stop();
1015
+ }
1016
+ }
1017
+ if (!screenStream.getTracks().some((track) => track.id === event.track.id)) {
1018
+ screenStream.addTrack(event.track);
1019
+ }
1020
+ event.track.onended = () => {
1021
+ try {
1022
+ screenStream?.removeTrack(event.track);
1023
+ } catch {
1024
+ }
1025
+ if (screenStream?.getTracks().length === 0) {
1026
+ this.remoteScreenStreams.delete(participantId);
1027
+ this.onScreenShareStopped(
1028
+ participantId,
1029
+ metadata.track_id
1030
+ );
1031
+ }
1032
+ };
1033
+ this.onScreenShareStarted(participantId, metadata);
1034
+ this.onRemoteTrack(event.track, screenStream, {
1035
+ ...metadata,
1036
+ source: "screen"
1037
+ });
1038
+ return;
1039
+ }
1040
+ if (event.track.kind === "video" && source !== "camera") {
1041
+ this.onError(
1042
+ new Error(
1043
+ `Refusing to route unknown remote video source to camera: ${metadata.track_id}`
1044
+ )
1045
+ );
1046
+ return;
1047
+ }
984
1048
  let remoteStream = this.remoteStreams.get(participantId);
985
1049
  if (!remoteStream) {
986
1050
  remoteStream = new MediaStream();
987
1051
  this.remoteStreams.set(participantId, remoteStream);
988
1052
  }
989
- const source = metadata.source || this.trackToSource.get(metadata.track_id);
990
- if (source === "screen") {
991
- for (const existing of remoteStream.getVideoTracks()) {
992
- if (existing.id !== event.track.id && this.trackToSource.get(existing.id) === "screen") {
993
- remoteStream.removeTrack(existing);
994
- existing.stop();
995
- }
996
- }
997
- } else {
998
- for (const existing of remoteStream.getTracks()) {
999
- if (existing.kind === event.track.kind && existing.id !== event.track.id) {
1000
- remoteStream.removeTrack(existing);
1001
- existing.stop();
1002
- }
1053
+ for (const existing of remoteStream.getTracks()) {
1054
+ if (existing.kind === event.track.kind && existing.id !== event.track.id) {
1055
+ remoteStream.removeTrack(existing);
1056
+ existing.stop();
1003
1057
  }
1004
1058
  }
1005
1059
  if (!remoteStream.getTracks().some((track) => track.id === event.track.id)) {
@@ -1011,31 +1065,24 @@ var SFUClient = class {
1011
1065
  } catch {
1012
1066
  }
1013
1067
  };
1014
- if (source === "screen") {
1015
- this.onScreenShareStarted(participantId, metadata);
1016
- }
1017
- this.onRemoteTrack(event.track, remoteStream, metadata);
1068
+ this.onRemoteTrack(event.track, remoteStream, {
1069
+ ...metadata,
1070
+ source: event.track.kind === "audio" ? "audio" : "camera"
1071
+ });
1018
1072
  }
1019
1073
  normalizeRemoteTrackId(browserTrackId) {
1020
1074
  return browserTrackId?.startsWith("track-") ? browserTrackId.slice("track-".length) : browserTrackId;
1021
1075
  }
1022
1076
  resolveRemoteTrackMetadata(track, stream) {
1023
- const sfuTrackId = this.normalizeRemoteTrackId(track.id);
1024
- const exact = this.trackMetadata.get(sfuTrackId);
1025
- if (exact) return exact;
1026
- const byTrackId = this.trackMetadata.get(track.id);
1027
- if (byTrackId) return byTrackId;
1028
- const matching = [...this.trackMetadata.values()].find(
1029
- (entry) => entry.kind === track.kind
1030
- );
1031
- if (matching) return matching;
1032
- if (stream?.id) {
1033
- const participant = this.participants.get(stream.id);
1034
- if (participant) {
1035
- return [...this.trackMetadata.values()].find(
1036
- (entry) => entry.participant_id === participant.participant_id && entry.kind === track.kind
1037
- ) || null;
1038
- }
1077
+ const candidates = [
1078
+ this.normalizeRemoteTrackId(track.id),
1079
+ track.id,
1080
+ stream?.id ? this.normalizeRemoteTrackId(stream.id) : null,
1081
+ stream?.id || null
1082
+ ].filter(Boolean);
1083
+ for (const id of candidates) {
1084
+ const metadata = this.trackMetadata.get(id);
1085
+ if (metadata) return metadata;
1039
1086
  }
1040
1087
  return null;
1041
1088
  }
@@ -1166,20 +1213,19 @@ var SFUClient = class {
1166
1213
  }
1167
1214
  async publishExistingTracks(audio = true, video = true) {
1168
1215
  if (!this.publisher || !this.localStream) return;
1169
- this.audioSender = null;
1170
- this.videoSender = null;
1171
- for (const track of this.localStream.getTracks()) {
1172
- if (track.kind === "audio" && !audio) continue;
1173
- if (track.kind === "video" && !video) continue;
1174
- const sender = this.publisher.addTrack(track, this.localStream);
1175
- if (track.kind === "audio") this.audioSender = sender;
1176
- if (track.kind === "video") this.videoSender = sender;
1177
- }
1178
- if (this.screenStream?.getVideoTracks()[0]) {
1179
- const screenTrack = this.screenStream.getVideoTracks()[0];
1180
- this.screenSender = this.publisher.addTrack(screenTrack, this.screenStream);
1181
- }
1182
- if (!this.publisher.getSenders().length) return;
1216
+ if (!this.audioTransceiver || !this.cameraTransceiver || !this.screenTransceiver) {
1217
+ throw new Error("Publisher transceivers are not initialized");
1218
+ }
1219
+ const audioTrack = this.localStream.getAudioTracks().find((track) => track.readyState === "live");
1220
+ const cameraTrack = this.localStream.getVideoTracks().find((track) => track.readyState === "live");
1221
+ const screenTrack = this.screenStream?.getVideoTracks().find((track) => track.readyState === "live");
1222
+ await this.audioSender?.replaceTrack(audio && audioTrack ? audioTrack : null);
1223
+ this.audioTransceiver.direction = audio && audioTrack ? "sendonly" : "inactive";
1224
+ await this.videoSender?.replaceTrack(video && cameraTrack ? cameraTrack : null);
1225
+ this.cameraTransceiver.direction = video && cameraTrack ? "sendonly" : "inactive";
1226
+ await this.screenSender?.replaceTrack(screenTrack || null);
1227
+ this.screenTransceiver.direction = screenTrack ? "sendonly" : "inactive";
1228
+ if (!audioTrack && !cameraTrack && !screenTrack) return;
1183
1229
  const offer = await this.publisher.createOffer();
1184
1230
  await this.publisher.setLocalDescription(offer);
1185
1231
  this.send({
@@ -1189,44 +1235,86 @@ var SFUClient = class {
1189
1235
  sdp: offer.sdp
1190
1236
  }
1191
1237
  });
1192
- if (this.screenStream?.getVideoTracks()[0]) {
1238
+ if (screenTrack) {
1193
1239
  this.send({ type: "start_screen_share" });
1194
1240
  }
1195
1241
  }
1196
1242
  async shareScreen() {
1197
- if (!this.publisher) throw new Error("Publisher PeerConnection is not initialized");
1198
- if (this.screenStream) return this.screenStream;
1199
- this.screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
1200
- const screenTrack = this.screenStream.getVideoTracks()[0];
1201
- if (!screenTrack) throw new Error("Screen share returned no video track");
1202
- this.screenSender = this.publisher.addTrack(screenTrack, this.screenStream);
1203
- const offer = await this.publisher.createOffer();
1204
- await this.publisher.setLocalDescription(offer);
1205
- this.send({
1206
- type: "publish_offer",
1207
- payload: {
1208
- revision: ++this.publisherRevision,
1209
- sdp: offer.sdp
1210
- }
1243
+ if (!this.publisher) {
1244
+ throw new Error("Publisher PeerConnection is not initialized");
1245
+ }
1246
+ if (!this.screenTransceiver || !this.screenSender) {
1247
+ throw new Error("Screen transceiver is not initialized");
1248
+ }
1249
+ if (this.screenStream) {
1250
+ return this.screenStream;
1251
+ }
1252
+ const screenStream = await navigator.mediaDevices.getDisplayMedia({
1253
+ video: true,
1254
+ audio: false
1211
1255
  });
1212
- this.send({ type: "start_screen_share" });
1213
- screenTrack.onended = () => {
1214
- void this.stopScreenShare();
1215
- };
1216
- return this.screenStream;
1217
- }
1218
- async stopScreenShare() {
1219
- if (!this.screenStream) return;
1220
- const screenTrack = this.screenStream.getVideoTracks()[0];
1221
- if (screenTrack) screenTrack.onended = null;
1222
- if (this.screenSender && this.publisher) {
1256
+ const screenTrack = screenStream.getVideoTracks()[0];
1257
+ if (!screenTrack) {
1258
+ screenStream.getTracks().forEach((track) => track.stop());
1259
+ throw new Error("Screen share returned no video track");
1260
+ }
1261
+ this.screenStream = screenStream;
1262
+ try {
1263
+ if (!this.send({ type: "start_screen_share" })) {
1264
+ throw new Error("Failed to signal start_screen_share");
1265
+ }
1266
+ await this.screenSender.replaceTrack(screenTrack);
1267
+ this.screenTransceiver.direction = "sendonly";
1268
+ const offer = await this.publisher.createOffer();
1269
+ await this.publisher.setLocalDescription(offer);
1270
+ this.send({
1271
+ type: "publish_offer",
1272
+ payload: {
1273
+ revision: ++this.publisherRevision,
1274
+ sdp: offer.sdp
1275
+ }
1276
+ });
1277
+ screenTrack.onended = () => {
1278
+ void this.stopScreenShare();
1279
+ };
1280
+ console.info("[publisher] Screen track attached", {
1281
+ screenTrackId: screenTrack.id,
1282
+ cameraTrackId: this.videoSender?.track?.id,
1283
+ screenMid: this.screenTransceiver.mid,
1284
+ cameraMid: this.cameraTransceiver?.mid
1285
+ });
1286
+ return this.screenStream;
1287
+ } catch (error) {
1223
1288
  try {
1224
- this.publisher.removeTrack(this.screenSender);
1289
+ await this.screenSender.replaceTrack(null);
1225
1290
  } catch {
1226
1291
  }
1227
- this.screenSender = null;
1292
+ this.screenTransceiver.direction = "inactive";
1293
+ screenStream.getTracks().forEach((track) => track.stop());
1294
+ this.screenStream = null;
1228
1295
  try {
1229
- const offer = await this.publisher.createOffer();
1296
+ this.send({ type: "stop_screen_share" });
1297
+ } catch {
1298
+ }
1299
+ throw error;
1300
+ }
1301
+ }
1302
+ async stopScreenShare() {
1303
+ if (!this.screenStream && !this.screenSender?.track) return;
1304
+ const screenTrack = this.screenSender?.track || null;
1305
+ if (screenTrack) screenTrack.onended = null;
1306
+ try {
1307
+ await this.screenSender?.replaceTrack(null);
1308
+ } catch (error) {
1309
+ this.onError(error);
1310
+ }
1311
+ if (this.screenTransceiver) {
1312
+ this.screenTransceiver.direction = "inactive";
1313
+ }
1314
+ this.send({ type: "stop_screen_share" });
1315
+ try {
1316
+ const offer = await this.publisher?.createOffer();
1317
+ if (offer && this.publisher) {
1230
1318
  await this.publisher.setLocalDescription(offer);
1231
1319
  this.send({
1232
1320
  type: "publish_offer",
@@ -1235,13 +1323,15 @@ var SFUClient = class {
1235
1323
  sdp: offer.sdp
1236
1324
  }
1237
1325
  });
1238
- this.send({ type: "stop_screen_share" });
1239
- } catch (error) {
1240
- this.onError(error);
1241
1326
  }
1327
+ } catch (error) {
1328
+ this.onError(error);
1242
1329
  }
1243
- this.screenStream.getTracks().forEach((track) => track.stop());
1330
+ this.screenStream?.getTracks().forEach((track) => track.stop());
1244
1331
  this.screenStream = null;
1332
+ if (this.currentParticipantId) {
1333
+ this.onScreenShareStopped(this.currentParticipantId);
1334
+ }
1245
1335
  }
1246
1336
  muteAudio(muted) {
1247
1337
  this.localStream?.getAudioTracks().forEach((track) => {
@@ -1327,6 +1417,11 @@ var SFUClient = class {
1327
1417
  stream.getTracks().forEach((track) => track.stop());
1328
1418
  this.remoteStreams.delete(participantId);
1329
1419
  }
1420
+ const screenStream = this.remoteScreenStreams.get(participantId);
1421
+ if (screenStream) {
1422
+ screenStream.getTracks().forEach((track) => track.stop());
1423
+ this.remoteScreenStreams.delete(participantId);
1424
+ }
1330
1425
  for (const [trackId, metadata] of this.trackMetadata) {
1331
1426
  if (metadata.participant_id === participantId) {
1332
1427
  this.trackMetadata.delete(trackId);
@@ -1402,6 +1497,9 @@ var SFUClient = class {
1402
1497
  this.videoSender = null;
1403
1498
  this.audioSender = null;
1404
1499
  this.screenSender = null;
1500
+ this.audioTransceiver = null;
1501
+ this.cameraTransceiver = null;
1502
+ this.screenTransceiver = null;
1405
1503
  this.pendingIce = { publisher: [], subscriber: [] };
1406
1504
  this.offerQueue = [];
1407
1505
  this.subscriberNegotiating = false;
@@ -1424,6 +1522,10 @@ var SFUClient = class {
1424
1522
  stream.getTracks().forEach((track) => track.stop());
1425
1523
  }
1426
1524
  this.remoteStreams.clear();
1525
+ for (const stream of this.remoteScreenStreams.values()) {
1526
+ stream.getTracks().forEach((track) => track.stop());
1527
+ }
1528
+ this.remoteScreenStreams.clear();
1427
1529
  this.trackMetadata.clear();
1428
1530
  this.trackToParticipant.clear();
1429
1531
  this.trackToSource.clear();
@@ -1464,7 +1566,7 @@ var SFUClient = class {
1464
1566
 
1465
1567
  // src/core/VideoCore.ts
1466
1568
  var VideoSDKCore = class {
1467
- constructor(events = {}, apiBase = SDK_CONFIG.apiBase || "https://api.example.com/api", wsBase = SDK_CONFIG.wsBase || "wss://api.example.com/ws") {
1569
+ constructor(events = {}, apiBase = SDK_CONFIG.apiBase, wsBase = SDK_CONFIG.wsBase) {
1468
1570
  this.events = events;
1469
1571
  this.apiBase = apiBase;
1470
1572
  this.wsBase = wsBase;
@@ -1480,6 +1582,8 @@ var VideoSDKCore = class {
1480
1582
  this.intentionalDisconnect = false;
1481
1583
  this.remoteStreams = /* @__PURE__ */ new Map();
1482
1584
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
1585
+ this.joinPromise = null;
1586
+ this.disconnectPromise = null;
1483
1587
  this.state = new MeetingState();
1484
1588
  this.events = events;
1485
1589
  this.activeSpeakerDetector = new ActiveSpeakerDetector({
@@ -1513,7 +1617,7 @@ var VideoSDKCore = class {
1513
1617
  this.events.onError?.(err);
1514
1618
  console.error("[MeetingSDK Error]", err);
1515
1619
  }
1516
- // ============ LOCAL MEDIA ============
1620
+ // LOCAL MEDIA
1517
1621
  async initLocal(video, name) {
1518
1622
  this.participantName = name;
1519
1623
  try {
@@ -1562,7 +1666,7 @@ var VideoSDKCore = class {
1562
1666
  });
1563
1667
  this.state.localStream = this.localStream;
1564
1668
  }
1565
- // ============ CONNECTION / SIGNALLING ============
1669
+ // ROOM MANAGEMENT
1566
1670
  async createRoom(input) {
1567
1671
  const client = this.sfuClient ?? new SFUClient({
1568
1672
  apiBase: this.apiBase,
@@ -1587,6 +1691,7 @@ var VideoSDKCore = class {
1587
1691
  });
1588
1692
  return client.deleteRoom(roomCode);
1589
1693
  }
1694
+ // JOIN
1590
1695
  async joinMeeting(config) {
1591
1696
  const {
1592
1697
  roomId,
@@ -1601,6 +1706,28 @@ var VideoSDKCore = class {
1601
1706
  if (userId) {
1602
1707
  this.myId = userId;
1603
1708
  }
1709
+ if (this.sfuClient && this.room.id === roomId && this.sfuClient.getConnectionState() === "connected") {
1710
+ console.info("[SDK] Already connected; duplicate join ignored", {
1711
+ roomId,
1712
+ userId: this.myId
1713
+ });
1714
+ return;
1715
+ }
1716
+ if (this.joinPromise) {
1717
+ console.info(
1718
+ "[SDK] Join already in progress; waiting for existing join",
1719
+ {
1720
+ roomId,
1721
+ userId: this.myId
1722
+ }
1723
+ );
1724
+ return this.joinPromise;
1725
+ }
1726
+ if (this.sfuClient && this.room.id && this.room.id !== roomId) {
1727
+ throw new Error(
1728
+ `Already connected to room ${this.room.id}. Leave the current meeting before joining another room.`
1729
+ );
1730
+ }
1604
1731
  this.intentionalDisconnect = false;
1605
1732
  this.participantName = name;
1606
1733
  this.room.id = roomId;
@@ -1622,19 +1749,43 @@ var VideoSDKCore = class {
1622
1749
  track.enabled = !videoMuted;
1623
1750
  });
1624
1751
  this.updateLocalMedia(audioMuted, videoMuted);
1625
- return this.connect(roomId, name);
1752
+ const promise = this.connect(roomId, name);
1753
+ this.joinPromise = promise;
1754
+ try {
1755
+ await promise;
1756
+ } finally {
1757
+ if (this.joinPromise === promise) {
1758
+ this.joinPromise = null;
1759
+ }
1760
+ }
1626
1761
  }
1762
+ // SFU CONNECTION
1627
1763
  async connect(roomId, name) {
1628
1764
  if (this.sfuClient) {
1629
- await this.sfuClient.leave();
1630
- this.sfuClient = null;
1765
+ const state = this.sfuClient.getConnectionState();
1766
+ if (this.room.id === roomId && (state === "connecting" || state === "connected" || state === "reconnecting")) {
1767
+ console.info(
1768
+ "[SDK] Existing SFU connection is active; connect ignored",
1769
+ {
1770
+ roomId,
1771
+ state
1772
+ }
1773
+ );
1774
+ return;
1775
+ }
1776
+ throw new Error(
1777
+ `SFU client already exists in state "${state}". Disconnect explicitly before creating another connection.`
1778
+ );
1631
1779
  }
1632
1780
  const client = new SFUClient({
1633
1781
  apiBase: this.apiBase,
1634
1782
  wsBase: this.wsBase,
1635
1783
  userId: this.myId,
1636
1784
  autoSubscribe: true,
1637
- onStateChange: (state) => this.handleSFUState(state),
1785
+ // CONNECTION STATE
1786
+ onStateChange: (state) => {
1787
+ this.handleSFUState(state);
1788
+ },
1638
1789
  onConnected: (payload, resumed) => {
1639
1790
  console.log(`[SFU] ${resumed ? "Resumed" : "Joined"} room`, payload);
1640
1791
  this.state.updateLocalParticipant({
@@ -1648,6 +1799,7 @@ var VideoSDKCore = class {
1648
1799
  onReconnecting: (attempt) => {
1649
1800
  console.warn(`[SFU] Reconnecting, attempt ${attempt}`);
1650
1801
  },
1802
+ // ADMISSION CONTROL
1651
1803
  onJoinPending: (info) => {
1652
1804
  console.log("[SFU] Join request pending", info);
1653
1805
  this.events.onEntryRequested?.({
@@ -1660,7 +1812,10 @@ var VideoSDKCore = class {
1660
1812
  console.log("[SFU] Join approved", requestId);
1661
1813
  this.events.onJoinApproved?.(requestId);
1662
1814
  this.events.onEntryResponded?.(
1663
- { participantId: this.myId, decision: "approved" },
1815
+ {
1816
+ participantId: this.myId,
1817
+ decision: "approved"
1818
+ },
1664
1819
  "approved"
1665
1820
  );
1666
1821
  },
@@ -1668,13 +1823,19 @@ var VideoSDKCore = class {
1668
1823
  console.warn("[SFU] Join rejected", requestId, reason);
1669
1824
  this.events.onJoinRejected?.(requestId);
1670
1825
  this.events.onEntryResponded?.(
1671
- { participantId: this.myId, decision: "rejected" },
1826
+ {
1827
+ participantId: this.myId,
1828
+ decision: "rejected"
1829
+ },
1672
1830
  "rejected"
1673
1831
  );
1674
1832
  this.emitError(
1675
1833
  "JOIN_REJECTED",
1676
1834
  reason || "Your request to join was rejected",
1677
- { requestId, reason },
1835
+ {
1836
+ requestId,
1837
+ reason
1838
+ },
1678
1839
  false
1679
1840
  );
1680
1841
  },
@@ -1682,16 +1843,21 @@ var VideoSDKCore = class {
1682
1843
  const requestId = request?.request_id || request?.requestId;
1683
1844
  const userId = request?.user_id || request?.userId;
1684
1845
  const nameValue = request?.user_metadata?.name || request?.display_name || request?.name || "Participant";
1685
- if (!requestId || !userId) return;
1846
+ if (!requestId || !userId) {
1847
+ return;
1848
+ }
1686
1849
  this.events.onEntryRequested?.({
1687
1850
  requestId,
1688
1851
  userId,
1689
1852
  name: nameValue
1690
1853
  });
1691
1854
  },
1855
+ // PARTICIPANTS
1692
1856
  onParticipantJoined: (participant) => {
1693
1857
  const pid = participant.participant_id;
1694
- if (!pid || pid === this.myId) return;
1858
+ if (!pid || pid === this.myId) {
1859
+ return;
1860
+ }
1695
1861
  const pname = participant.user_metadata?.name || participant.display_name || "Participant";
1696
1862
  this.state.addParticipant({
1697
1863
  id: pid,
@@ -1710,9 +1876,13 @@ var VideoSDKCore = class {
1710
1876
  });
1711
1877
  },
1712
1878
  onParticipantPresence: (participantId, presence) => {
1713
- if (participantId === this.myId) return;
1879
+ if (participantId === this.myId) {
1880
+ return;
1881
+ }
1714
1882
  const current = this.state.getParticipant(participantId);
1715
- if (!current) return;
1883
+ if (!current) {
1884
+ return;
1885
+ }
1716
1886
  const connectionState = presence === "reconnecting" ? "reconnecting" : presence === "connected" || presence === "online" ? "connected" : presence === "disconnected" ? "disconnected" : current.connectionState;
1717
1887
  this.state.updateParticipantMedia(participantId, {
1718
1888
  stream: current.media?.stream ?? null
@@ -1728,19 +1898,24 @@ var VideoSDKCore = class {
1728
1898
  },
1729
1899
  onParticipantLeft: (participant) => {
1730
1900
  const pid = typeof participant === "string" ? participant : participant.participant_id;
1731
- if (!pid) return;
1901
+ if (!pid) {
1902
+ return;
1903
+ }
1732
1904
  console.log("[SFU] Participant permanently left", pid);
1733
1905
  this.cleanupRemoteParticipant(pid);
1734
1906
  this.state.removeParticipant(pid);
1735
1907
  this.events.onUserLeft?.(pid);
1736
1908
  },
1909
+ // TRACKS
1737
1910
  onTrackPublished: (track) => {
1738
1911
  console.log("[SFU] Track published", track);
1739
1912
  },
1740
1913
  onTrackUnpublished: (track) => {
1741
1914
  console.log("[SFU] Track unpublished", track);
1742
1915
  const pid = track.participant_id;
1743
- if (!pid) return;
1916
+ if (!pid) {
1917
+ return;
1918
+ }
1744
1919
  const current = this.state.getParticipant(pid);
1745
1920
  if (track.source === "screen") {
1746
1921
  const screen = this.remoteScreenStreams.get(pid);
@@ -1754,7 +1929,9 @@ var VideoSDKCore = class {
1754
1929
  this.events.onScreenShareStopped?.(pid);
1755
1930
  return;
1756
1931
  }
1757
- if (!current) return;
1932
+ if (!current) {
1933
+ return;
1934
+ }
1758
1935
  this.state.updateParticipantMedia(pid, {
1759
1936
  stream: current.media?.stream ?? null,
1760
1937
  micEnabled: track.kind === "audio" ? false : current.media?.micEnabled ?? true,
@@ -1763,9 +1940,13 @@ var VideoSDKCore = class {
1763
1940
  },
1764
1941
  onTrackStateChanged: (track) => {
1765
1942
  const pid = track.participant_id;
1766
- if (!pid || pid === this.myId) return;
1943
+ if (!pid || pid === this.myId) {
1944
+ return;
1945
+ }
1767
1946
  const current = this.state.getParticipant(pid);
1768
- if (!current) return;
1947
+ if (!current) {
1948
+ return;
1949
+ }
1769
1950
  if (track.source === "audio") {
1770
1951
  this.state.updateParticipantMedia(pid, {
1771
1952
  stream: current.media?.stream ?? null,
@@ -1781,17 +1962,22 @@ var VideoSDKCore = class {
1781
1962
  }
1782
1963
  this.events.onTrackStateChanged?.(track);
1783
1964
  },
1965
+ // RECORDING
1784
1966
  onRecordingStarted: (recording) => {
1785
1967
  this.events.onRecordingStarted?.(recording);
1786
1968
  },
1787
1969
  onRecordingStopped: (recording) => {
1788
1970
  this.events.onRecordingStopped?.(recording);
1789
1971
  },
1972
+ // REMOTE MEDIA
1790
1973
  onRemoteTrack: (track, stream, metadata) => {
1791
1974
  this.handleRemoteTrack(track, stream, metadata);
1792
1975
  },
1976
+ // SCREEN SHARE
1793
1977
  onScreenShareStarted: (participantId, track) => {
1794
- if (participantId === this.myId) return;
1978
+ if (participantId === this.myId) {
1979
+ return;
1980
+ }
1795
1981
  console.log("[SFU] Screen share started", participantId, track);
1796
1982
  const current = this.state.getParticipant(participantId);
1797
1983
  if (current) {
@@ -1802,7 +1988,9 @@ var VideoSDKCore = class {
1802
1988
  }
1803
1989
  },
1804
1990
  onScreenShareStopped: (participantId) => {
1805
- if (participantId === this.myId) return;
1991
+ if (participantId === this.myId) {
1992
+ return;
1993
+ }
1806
1994
  const screen = this.remoteScreenStreams.get(participantId);
1807
1995
  screen?.getTracks().forEach((track) => track.stop());
1808
1996
  this.remoteScreenStreams.delete(participantId);
@@ -1813,6 +2001,7 @@ var VideoSDKCore = class {
1813
2001
  });
1814
2002
  this.events.onScreenShareStopped?.(participantId);
1815
2003
  },
2004
+ // ERROR
1816
2005
  onError: (error) => {
1817
2006
  console.error("[SFU] Error:", error);
1818
2007
  this.emitError(
@@ -1825,11 +2014,12 @@ var VideoSDKCore = class {
1825
2014
  });
1826
2015
  this.sfuClient = client;
1827
2016
  client.setLocalStream(this.localStream);
1828
- return client.getToken(roomId, {
2017
+ await client.getToken(roomId, {
1829
2018
  displayName: name,
1830
2019
  userId: this.myId
1831
2020
  });
1832
2021
  }
2022
+ // SFU STATE
1833
2023
  handleSFUState(state) {
1834
2024
  this.events.onConnectionStateChanged?.(state);
1835
2025
  if (state === "reconnecting") {
@@ -1848,9 +2038,12 @@ var VideoSDKCore = class {
1848
2038
  );
1849
2039
  }
1850
2040
  }
2041
+ // REMOTE TRACKS
1851
2042
  handleRemoteTrack(track, stream, metadata) {
1852
2043
  const participantId = metadata?.participant_id;
1853
- if (!participantId || participantId === this.myId) return;
2044
+ if (!participantId || participantId === this.myId) {
2045
+ return;
2046
+ }
1854
2047
  const source = metadata.source;
1855
2048
  if (source === "screen") {
1856
2049
  let screenStream = this.remoteScreenStreams.get(participantId);
@@ -1901,36 +2094,41 @@ var VideoSDKCore = class {
1901
2094
  this.events.onTrack?.(remoteStream, participantId);
1902
2095
  track.onended = () => {
1903
2096
  if (track.kind === "video") {
1904
- const participant2 = this.state.getParticipant(participantId);
1905
2097
  this.state.updateParticipantMedia(participantId, {
1906
2098
  camEnabled: false,
1907
2099
  stream: remoteStream
1908
2100
  });
1909
2101
  return;
1910
2102
  }
1911
- const participant = this.state.getParticipant(participantId);
1912
2103
  this.state.updateParticipantMedia(participantId, {
1913
2104
  micEnabled: false,
1914
2105
  stream: remoteStream
1915
2106
  });
1916
2107
  };
1917
2108
  }
2109
+ // MEETING
1918
2110
  getMeeting() {
1919
2111
  return this.room;
1920
2112
  }
1921
- // ============ ADMISSION CONTROL ============
2113
+ // ADMISSION CONTROL
1922
2114
  async approveJoinRequest(requestId) {
1923
- if (!this.sfuClient) throw new Error("SFU client is not connected");
2115
+ if (!this.sfuClient) {
2116
+ throw new Error("SFU client is not connected");
2117
+ }
1924
2118
  return this.sfuClient.approveJoinRequest(requestId);
1925
2119
  }
1926
2120
  async rejectJoinRequest(requestId, reason) {
1927
- if (!this.sfuClient) throw new Error("SFU client is not connected");
2121
+ if (!this.sfuClient) {
2122
+ throw new Error("SFU client is not connected");
2123
+ }
1928
2124
  return this.sfuClient.rejectJoinRequest(requestId, reason);
1929
2125
  }
1930
- // ============ MEDIA CONTROL ============
2126
+ // MEDIA CONTROL
1931
2127
  toggleMic() {
1932
2128
  const mediaState = this.state.localParticipant?.media;
1933
- if (!mediaState || !this.localStream) return;
2129
+ if (!mediaState || !this.localStream) {
2130
+ return;
2131
+ }
1934
2132
  const nextEnabled = !mediaState.micEnabled;
1935
2133
  this.localStream.getAudioTracks().forEach((track) => {
1936
2134
  track.enabled = nextEnabled;
@@ -1938,14 +2136,18 @@ var VideoSDKCore = class {
1938
2136
  this.state.updateLocalParticipant({
1939
2137
  id: this.myId,
1940
2138
  name: this.participantName,
1941
- media: { micEnabled: nextEnabled }
2139
+ media: {
2140
+ micEnabled: nextEnabled
2141
+ }
1942
2142
  });
1943
2143
  this.sfuClient?.muteAudio(!nextEnabled);
1944
2144
  this.events.onMicToggled?.(this.myId, nextEnabled);
1945
2145
  }
1946
2146
  toggleCam() {
1947
2147
  const mediaState = this.state.localParticipant?.media;
1948
- if (!mediaState || !this.localStream) return;
2148
+ if (!mediaState || !this.localStream) {
2149
+ return;
2150
+ }
1949
2151
  const nextEnabled = !mediaState.camEnabled;
1950
2152
  this.localStream.getVideoTracks().forEach((track) => {
1951
2153
  track.enabled = nextEnabled;
@@ -1953,12 +2155,14 @@ var VideoSDKCore = class {
1953
2155
  this.state.updateLocalParticipant({
1954
2156
  id: this.myId,
1955
2157
  name: this.participantName,
1956
- media: { camEnabled: nextEnabled }
2158
+ media: {
2159
+ camEnabled: nextEnabled
2160
+ }
1957
2161
  });
1958
2162
  this.sfuClient?.muteVideo(!nextEnabled);
1959
2163
  this.events.onCamToggled?.(this.myId, nextEnabled);
1960
2164
  }
1961
- // ============ SCREEN SHARING ============
2165
+ // SCREEN SHARING
1962
2166
  async startScreenShare() {
1963
2167
  try {
1964
2168
  if (!navigator.mediaDevices?.getDisplayMedia) {
@@ -1990,7 +2194,9 @@ var VideoSDKCore = class {
1990
2194
  }
1991
2195
  }
1992
2196
  stopScreenShare() {
1993
- if (!this.screenStream) return;
2197
+ if (!this.screenStream) {
2198
+ return;
2199
+ }
1994
2200
  void this.sfuClient?.stopScreenShare();
1995
2201
  this.screenStream.getTracks().forEach((track) => track.stop());
1996
2202
  this.screenStream = null;
@@ -2003,7 +2209,7 @@ var VideoSDKCore = class {
2003
2209
  }
2004
2210
  });
2005
2211
  }
2006
- // ============ RECORDING ============
2212
+ // RECORDING
2007
2213
  async startRecording() {
2008
2214
  if (!this.sfuClient) {
2009
2215
  throw new Error("SFU client is not connected");
@@ -2022,7 +2228,7 @@ var VideoSDKCore = class {
2022
2228
  }
2023
2229
  return this.sfuClient.getRecordingStatus();
2024
2230
  }
2025
- // ============ CHAT ============
2231
+ // CHAT
2026
2232
  sendChatMessage(payload) {
2027
2233
  if (!this.sfuClient) {
2028
2234
  console.warn("SFU Client not connected");
@@ -2041,7 +2247,7 @@ var VideoSDKCore = class {
2041
2247
  this.state.addChatMessage(msg);
2042
2248
  this.events.onChatMessage?.(msg);
2043
2249
  }
2044
- // ============ CLEANUP ============
2250
+ // CLEANUP
2045
2251
  cleanupRemoteParticipant(participantId) {
2046
2252
  this.activeSpeakerDetector.removeParticipant(participantId);
2047
2253
  const stream = this.remoteStreams.get(participantId);
@@ -2051,16 +2257,34 @@ var VideoSDKCore = class {
2051
2257
  screen?.getTracks().forEach((track) => track.stop());
2052
2258
  this.remoteScreenStreams.delete(participantId);
2053
2259
  }
2260
+ // DISCONNECT
2054
2261
  async disconnect() {
2262
+ if (this.disconnectPromise) {
2263
+ return this.disconnectPromise;
2264
+ }
2265
+ this.disconnectPromise = this.performDisconnect();
2266
+ try {
2267
+ await this.disconnectPromise;
2268
+ } finally {
2269
+ this.disconnectPromise = null;
2270
+ }
2271
+ }
2272
+ async performDisconnect() {
2055
2273
  this.intentionalDisconnect = true;
2056
- await this.sfuClient?.leave();
2274
+ this.joinPromise = null;
2275
+ const client = this.sfuClient;
2057
2276
  this.sfuClient = null;
2277
+ if (client) {
2278
+ await client.leave();
2279
+ }
2058
2280
  if (this.localStream) {
2059
2281
  this.localStream.getTracks().forEach((track) => track.stop());
2060
2282
  this.localStream = null;
2061
2283
  }
2062
- this.screenStream?.getTracks().forEach((track) => track.stop());
2063
- this.screenStream = null;
2284
+ if (this.screenStream) {
2285
+ this.screenStream.getTracks().forEach((track) => track.stop());
2286
+ this.screenStream = null;
2287
+ }
2064
2288
  this.isScreenSharing = false;
2065
2289
  for (const stream of this.remoteStreams.values()) {
2066
2290
  stream.getTracks().forEach((track) => track.stop());