@defcomm/meetingsdk 0.1.0 → 0.1.2

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
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ActiveSpeakerDetector: () => ActiveSpeakerDetector,
23
24
  MeetingProvider: () => MeetingProvider,
24
25
  MeetingState: () => MeetingState,
25
26
  SFUClient: () => SFUClient,
@@ -41,9 +42,7 @@ var import_react2 = require("react");
41
42
 
42
43
  // src/config/ws.ts
43
44
  var SDK_CONFIG = {
44
- // SFU API endpoint for token generation
45
45
  apiBase: "https://sfu-server.duckdns.org/api",
46
- // SFU WebSocket endpoint for signaling
47
46
  wsBase: "wss://sfu-server.duckdns.org/ws",
48
47
  // Public STUN/TURN servers (fallback)
49
48
  iceServers: [
@@ -52,10 +51,170 @@ var SDK_CONFIG = {
52
51
  ],
53
52
  // Connection and reconnection settings
54
53
  connectionTimeout: 1e4,
55
- maxReconnectAttempts: 5,
56
- // Legacy WebSocket URL (if using the old mesh architecture)
57
- wsUrl: "wss://sfu-server.duckdns.org/ws"
58
- // kept for backward compatibility
54
+ maxReconnectAttempts: 5
55
+ };
56
+
57
+ // src/core/ActiveSpeakerDetector.ts
58
+ var ActiveSpeakerDetector = class {
59
+ constructor(options = {}) {
60
+ this.audioContext = null;
61
+ this.speakers = /* @__PURE__ */ new Map();
62
+ this.timer = null;
63
+ this.lastEmitted = "";
64
+ this.intervalMs = options.intervalMs ?? 100;
65
+ this.threshold = options.threshold ?? 0.025;
66
+ this.releaseThreshold = options.releaseThreshold ?? 0.015;
67
+ this.releaseMs = options.releaseMs ?? 600;
68
+ this.onActiveSpeakersChanged = options.onActiveSpeakersChanged ?? (() => {
69
+ });
70
+ }
71
+ ensureAudioContext() {
72
+ if (!this.audioContext) {
73
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
74
+ if (!AudioContextClass) {
75
+ throw new Error("Web Audio API is not supported by this browser");
76
+ }
77
+ this.audioContext = new AudioContextClass();
78
+ }
79
+ return this.audioContext;
80
+ }
81
+ async resume() {
82
+ if (!this.audioContext) return;
83
+ if (this.audioContext.state === "suspended") {
84
+ try {
85
+ await this.audioContext.resume();
86
+ } catch {
87
+ }
88
+ }
89
+ }
90
+ addTrack(participantId, track) {
91
+ if (!participantId) return;
92
+ if (track.kind !== "audio") return;
93
+ this.removeParticipant(participantId);
94
+ const context = this.ensureAudioContext();
95
+ const stream = new MediaStream([track]);
96
+ const source = context.createMediaStreamSource(stream);
97
+ const analyser = context.createAnalyser();
98
+ analyser.fftSize = 512;
99
+ analyser.smoothingTimeConstant = 0.65;
100
+ analyser.minDecibels = -90;
101
+ analyser.maxDecibels = -10;
102
+ source.connect(analyser);
103
+ const data = new Float32Array(analyser.fftSize);
104
+ const state = {
105
+ participantId,
106
+ track,
107
+ stream,
108
+ source,
109
+ analyser,
110
+ data,
111
+ active: false,
112
+ lastActiveAt: 0
113
+ };
114
+ this.speakers.set(participantId, state);
115
+ track.addEventListener(
116
+ "ended",
117
+ () => {
118
+ this.removeParticipant(participantId);
119
+ },
120
+ { once: true }
121
+ );
122
+ this.start();
123
+ void this.resume();
124
+ }
125
+ removeParticipant(participantId) {
126
+ const state = this.speakers.get(participantId);
127
+ if (!state) return;
128
+ state.active = false;
129
+ try {
130
+ state.source.disconnect();
131
+ } catch {
132
+ }
133
+ state.stream.getTracks().forEach((track) => {
134
+ if (track !== state.track) {
135
+ track.stop();
136
+ }
137
+ });
138
+ this.speakers.delete(participantId);
139
+ this.emitIfChanged();
140
+ }
141
+ removeTrack(track) {
142
+ for (const [participantId, state] of this.speakers) {
143
+ if (state.track === track) {
144
+ this.removeParticipant(participantId);
145
+ }
146
+ }
147
+ }
148
+ clear() {
149
+ for (const participantId of this.speakers.keys()) {
150
+ this.removeParticipant(participantId);
151
+ }
152
+ this.speakers.clear();
153
+ this.stop();
154
+ this.emitIfChanged(true);
155
+ }
156
+ start() {
157
+ if (this.timer) return;
158
+ this.timer = setInterval(() => this.sample(), this.intervalMs);
159
+ }
160
+ stop() {
161
+ if (!this.timer) return;
162
+ clearInterval(this.timer);
163
+ this.timer = null;
164
+ }
165
+ sample() {
166
+ const now = performance.now();
167
+ for (const state of this.speakers.values()) {
168
+ if (state.track.readyState !== "live" || state.track.muted || !state.track.enabled) {
169
+ if (state.active) {
170
+ state.active = false;
171
+ }
172
+ continue;
173
+ }
174
+ state.analyser.getFloatTimeDomainData(state.data);
175
+ let sumSquares = 0;
176
+ for (let i = 0; i < state.data.length; i++) {
177
+ const sample = state.data[i];
178
+ sumSquares += sample * sample;
179
+ }
180
+ const rms = Math.sqrt(sumSquares / state.data.length);
181
+ if (!state.active) {
182
+ if (rms >= this.threshold) {
183
+ state.active = true;
184
+ state.lastActiveAt = now;
185
+ }
186
+ } else {
187
+ if (rms >= this.releaseThreshold) {
188
+ state.lastActiveAt = now;
189
+ } else if (now - state.lastActiveAt >= this.releaseMs) {
190
+ state.active = false;
191
+ }
192
+ }
193
+ }
194
+ this.emitIfChanged();
195
+ }
196
+ getActiveSpeakerIds() {
197
+ return Array.from(this.speakers.values()).filter((speaker) => speaker.active).map((speaker) => speaker.participantId);
198
+ }
199
+ emitIfChanged(force = false) {
200
+ const ids = this.getActiveSpeakerIds().sort();
201
+ const key = ids.join("|");
202
+ if (!force && key === this.lastEmitted) {
203
+ return;
204
+ }
205
+ this.lastEmitted = key;
206
+ this.onActiveSpeakersChanged(ids);
207
+ }
208
+ async destroy() {
209
+ this.clear();
210
+ if (this.audioContext) {
211
+ try {
212
+ await this.audioContext.close();
213
+ } catch {
214
+ }
215
+ this.audioContext = null;
216
+ }
217
+ }
59
218
  };
60
219
 
61
220
  // src/core/MeetingState.ts
@@ -66,6 +225,7 @@ var MeetingState = class {
66
225
  this.localStream = null;
67
226
  this.chatMessages = /* @__PURE__ */ new Map();
68
227
  this.presenterId = null;
228
+ this.activeSpeakerIds = /* @__PURE__ */ new Set();
69
229
  this.listeners = /* @__PURE__ */ new Map();
70
230
  }
71
231
  // ---- reactive system ----
@@ -87,6 +247,14 @@ var MeetingState = class {
87
247
  this.notify("presenter");
88
248
  this.notify("participants");
89
249
  }
250
+ setActiveSpeakers(ids) {
251
+ const next = new Set(ids);
252
+ if (next.size === this.activeSpeakerIds.size && [...next].every((id) => this.activeSpeakerIds.has(id))) {
253
+ return;
254
+ }
255
+ this.activeSpeakerIds = next;
256
+ this.notify("activeSpeakers");
257
+ }
90
258
  // ---- participants ----
91
259
  addParticipant(p) {
92
260
  if (this.participants.has(p.id)) return false;
@@ -200,9 +368,11 @@ var MeetingState = class {
200
368
  this.participants.clear();
201
369
  this.chatMessages.clear();
202
370
  this.presenterId = null;
371
+ this.activeSpeakerIds = /* @__PURE__ */ new Set();
203
372
  this.notify("participants");
204
373
  this.notify("chat");
205
374
  this.notify("presenter");
375
+ this.notify("activeSpeakers");
206
376
  }
207
377
  };
208
378
 
@@ -1330,7 +1500,7 @@ var SFUClient = class {
1330
1500
 
1331
1501
  // src/core/VideoCore.ts
1332
1502
  var VideoSDKCore = class {
1333
- constructor(events = {}, apiBase = SDK_CONFIG.apiBase || "https://api.example.com/api", wsBase = SDK_CONFIG.wsBase || "wss://api.example.com/ws") {
1503
+ constructor(events = {}, apiBase = SDK_CONFIG.apiBase, wsBase = SDK_CONFIG.wsBase) {
1334
1504
  this.events = events;
1335
1505
  this.apiBase = apiBase;
1336
1506
  this.wsBase = wsBase;
@@ -1346,8 +1516,16 @@ var VideoSDKCore = class {
1346
1516
  this.intentionalDisconnect = false;
1347
1517
  this.remoteStreams = /* @__PURE__ */ new Map();
1348
1518
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
1519
+ this.joinPromise = null;
1520
+ this.disconnectPromise = null;
1349
1521
  this.state = new MeetingState();
1350
1522
  this.events = events;
1523
+ this.activeSpeakerDetector = new ActiveSpeakerDetector({
1524
+ onActiveSpeakersChanged: (participantIds) => {
1525
+ this.state.setActiveSpeakers(participantIds);
1526
+ this.events.onActiveSpeakersChanged?.(participantIds);
1527
+ }
1528
+ });
1351
1529
  let storedId = null;
1352
1530
  try {
1353
1531
  storedId = typeof localStorage !== "undefined" ? localStorage.getItem("defcomm:participant_id") : null;
@@ -1373,7 +1551,7 @@ var VideoSDKCore = class {
1373
1551
  this.events.onError?.(err);
1374
1552
  console.error("[MeetingSDK Error]", err);
1375
1553
  }
1376
- // ============ LOCAL MEDIA ============
1554
+ // LOCAL MEDIA
1377
1555
  async initLocal(video, name) {
1378
1556
  this.participantName = name;
1379
1557
  try {
@@ -1388,6 +1566,10 @@ var VideoSDKCore = class {
1388
1566
  autoGainControl: true
1389
1567
  }
1390
1568
  });
1569
+ const localAudioTrack = this.localStream.getAudioTracks()[0];
1570
+ if (localAudioTrack) {
1571
+ this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
1572
+ }
1391
1573
  const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
1392
1574
  const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
1393
1575
  if (!hasVideo || !hasAudio) {
@@ -1418,7 +1600,7 @@ var VideoSDKCore = class {
1418
1600
  });
1419
1601
  this.state.localStream = this.localStream;
1420
1602
  }
1421
- // ============ CONNECTION / SIGNALLING ============
1603
+ // ROOM MANAGEMENT
1422
1604
  async createRoom(input) {
1423
1605
  const client = this.sfuClient ?? new SFUClient({
1424
1606
  apiBase: this.apiBase,
@@ -1443,11 +1625,43 @@ var VideoSDKCore = class {
1443
1625
  });
1444
1626
  return client.deleteRoom(roomCode);
1445
1627
  }
1628
+ // JOIN
1446
1629
  async joinMeeting(config) {
1447
- const { roomId, name, audioMuted = false, videoMuted = false } = config;
1630
+ const {
1631
+ roomId,
1632
+ name,
1633
+ audioMuted = false,
1634
+ videoMuted = false,
1635
+ userId
1636
+ } = config;
1448
1637
  if (!roomId || !name) {
1449
1638
  throw new Error("roomId and name are required to join meeting");
1450
1639
  }
1640
+ if (userId) {
1641
+ this.myId = userId;
1642
+ }
1643
+ if (this.sfuClient && this.room.id === roomId && this.sfuClient.getConnectionState() === "connected") {
1644
+ console.info("[SDK] Already connected; duplicate join ignored", {
1645
+ roomId,
1646
+ userId: this.myId
1647
+ });
1648
+ return;
1649
+ }
1650
+ if (this.joinPromise) {
1651
+ console.info(
1652
+ "[SDK] Join already in progress; waiting for existing join",
1653
+ {
1654
+ roomId,
1655
+ userId: this.myId
1656
+ }
1657
+ );
1658
+ return this.joinPromise;
1659
+ }
1660
+ if (this.sfuClient && this.room.id && this.room.id !== roomId) {
1661
+ throw new Error(
1662
+ `Already connected to room ${this.room.id}. Leave the current meeting before joining another room.`
1663
+ );
1664
+ }
1451
1665
  this.intentionalDisconnect = false;
1452
1666
  this.participantName = name;
1453
1667
  this.room.id = roomId;
@@ -1457,6 +1671,10 @@ var VideoSDKCore = class {
1457
1671
  video: true,
1458
1672
  audio: true
1459
1673
  });
1674
+ const localAudioTrack = this.localStream.getAudioTracks()[0];
1675
+ if (localAudioTrack) {
1676
+ this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
1677
+ }
1460
1678
  }
1461
1679
  this.localStream.getAudioTracks().forEach((track) => {
1462
1680
  track.enabled = !audioMuted;
@@ -1465,24 +1683,45 @@ var VideoSDKCore = class {
1465
1683
  track.enabled = !videoMuted;
1466
1684
  });
1467
1685
  this.updateLocalMedia(audioMuted, videoMuted);
1468
- return this.connect(roomId, name);
1686
+ const promise = this.connect(roomId, name);
1687
+ this.joinPromise = promise;
1688
+ try {
1689
+ await promise;
1690
+ } finally {
1691
+ if (this.joinPromise === promise) {
1692
+ this.joinPromise = null;
1693
+ }
1694
+ }
1469
1695
  }
1696
+ // SFU CONNECTION
1470
1697
  async connect(roomId, name) {
1471
1698
  if (this.sfuClient) {
1472
- await this.sfuClient.leave();
1473
- this.sfuClient = null;
1699
+ const state = this.sfuClient.getConnectionState();
1700
+ if (this.room.id === roomId && (state === "connecting" || state === "connected" || state === "reconnecting")) {
1701
+ console.info(
1702
+ "[SDK] Existing SFU connection is active; connect ignored",
1703
+ {
1704
+ roomId,
1705
+ state
1706
+ }
1707
+ );
1708
+ return;
1709
+ }
1710
+ throw new Error(
1711
+ `SFU client already exists in state "${state}". Disconnect explicitly before creating another connection.`
1712
+ );
1474
1713
  }
1475
1714
  const client = new SFUClient({
1476
1715
  apiBase: this.apiBase,
1477
1716
  wsBase: this.wsBase,
1478
1717
  userId: this.myId,
1479
1718
  autoSubscribe: true,
1480
- onStateChange: (state) => this.handleSFUState(state),
1719
+ // CONNECTION STATE
1720
+ onStateChange: (state) => {
1721
+ this.handleSFUState(state);
1722
+ },
1481
1723
  onConnected: (payload, resumed) => {
1482
- console.log(
1483
- `[SFU] ${resumed ? "Resumed" : "Joined"} room`,
1484
- payload
1485
- );
1724
+ console.log(`[SFU] ${resumed ? "Resumed" : "Joined"} room`, payload);
1486
1725
  this.state.updateLocalParticipant({
1487
1726
  id: this.myId,
1488
1727
  name: this.participantName
@@ -1494,6 +1733,7 @@ var VideoSDKCore = class {
1494
1733
  onReconnecting: (attempt) => {
1495
1734
  console.warn(`[SFU] Reconnecting, attempt ${attempt}`);
1496
1735
  },
1736
+ // ADMISSION CONTROL
1497
1737
  onJoinPending: (info) => {
1498
1738
  console.log("[SFU] Join request pending", info);
1499
1739
  this.events.onEntryRequested?.({
@@ -1506,7 +1746,10 @@ var VideoSDKCore = class {
1506
1746
  console.log("[SFU] Join approved", requestId);
1507
1747
  this.events.onJoinApproved?.(requestId);
1508
1748
  this.events.onEntryResponded?.(
1509
- { participantId: this.myId, decision: "approved" },
1749
+ {
1750
+ participantId: this.myId,
1751
+ decision: "approved"
1752
+ },
1510
1753
  "approved"
1511
1754
  );
1512
1755
  },
@@ -1514,13 +1757,19 @@ var VideoSDKCore = class {
1514
1757
  console.warn("[SFU] Join rejected", requestId, reason);
1515
1758
  this.events.onJoinRejected?.(requestId);
1516
1759
  this.events.onEntryResponded?.(
1517
- { participantId: this.myId, decision: "rejected" },
1760
+ {
1761
+ participantId: this.myId,
1762
+ decision: "rejected"
1763
+ },
1518
1764
  "rejected"
1519
1765
  );
1520
1766
  this.emitError(
1521
1767
  "JOIN_REJECTED",
1522
1768
  reason || "Your request to join was rejected",
1523
- { requestId, reason },
1769
+ {
1770
+ requestId,
1771
+ reason
1772
+ },
1524
1773
  false
1525
1774
  );
1526
1775
  },
@@ -1528,16 +1777,21 @@ var VideoSDKCore = class {
1528
1777
  const requestId = request?.request_id || request?.requestId;
1529
1778
  const userId = request?.user_id || request?.userId;
1530
1779
  const nameValue = request?.user_metadata?.name || request?.display_name || request?.name || "Participant";
1531
- if (!requestId || !userId) return;
1780
+ if (!requestId || !userId) {
1781
+ return;
1782
+ }
1532
1783
  this.events.onEntryRequested?.({
1533
1784
  requestId,
1534
1785
  userId,
1535
1786
  name: nameValue
1536
1787
  });
1537
1788
  },
1789
+ // PARTICIPANTS
1538
1790
  onParticipantJoined: (participant) => {
1539
1791
  const pid = participant.participant_id;
1540
- if (!pid || pid === this.myId) return;
1792
+ if (!pid || pid === this.myId) {
1793
+ return;
1794
+ }
1541
1795
  const pname = participant.user_metadata?.name || participant.display_name || "Participant";
1542
1796
  this.state.addParticipant({
1543
1797
  id: pid,
@@ -1556,9 +1810,13 @@ var VideoSDKCore = class {
1556
1810
  });
1557
1811
  },
1558
1812
  onParticipantPresence: (participantId, presence) => {
1559
- if (participantId === this.myId) return;
1813
+ if (participantId === this.myId) {
1814
+ return;
1815
+ }
1560
1816
  const current = this.state.getParticipant(participantId);
1561
- if (!current) return;
1817
+ if (!current) {
1818
+ return;
1819
+ }
1562
1820
  const connectionState = presence === "reconnecting" ? "reconnecting" : presence === "connected" || presence === "online" ? "connected" : presence === "disconnected" ? "disconnected" : current.connectionState;
1563
1821
  this.state.updateParticipantMedia(participantId, {
1564
1822
  stream: current.media?.stream ?? null
@@ -1574,19 +1832,24 @@ var VideoSDKCore = class {
1574
1832
  },
1575
1833
  onParticipantLeft: (participant) => {
1576
1834
  const pid = typeof participant === "string" ? participant : participant.participant_id;
1577
- if (!pid) return;
1835
+ if (!pid) {
1836
+ return;
1837
+ }
1578
1838
  console.log("[SFU] Participant permanently left", pid);
1579
1839
  this.cleanupRemoteParticipant(pid);
1580
1840
  this.state.removeParticipant(pid);
1581
1841
  this.events.onUserLeft?.(pid);
1582
1842
  },
1843
+ // TRACKS
1583
1844
  onTrackPublished: (track) => {
1584
1845
  console.log("[SFU] Track published", track);
1585
1846
  },
1586
1847
  onTrackUnpublished: (track) => {
1587
1848
  console.log("[SFU] Track unpublished", track);
1588
1849
  const pid = track.participant_id;
1589
- if (!pid) return;
1850
+ if (!pid) {
1851
+ return;
1852
+ }
1590
1853
  const current = this.state.getParticipant(pid);
1591
1854
  if (track.source === "screen") {
1592
1855
  const screen = this.remoteScreenStreams.get(pid);
@@ -1600,7 +1863,9 @@ var VideoSDKCore = class {
1600
1863
  this.events.onScreenShareStopped?.(pid);
1601
1864
  return;
1602
1865
  }
1603
- if (!current) return;
1866
+ if (!current) {
1867
+ return;
1868
+ }
1604
1869
  this.state.updateParticipantMedia(pid, {
1605
1870
  stream: current.media?.stream ?? null,
1606
1871
  micEnabled: track.kind === "audio" ? false : current.media?.micEnabled ?? true,
@@ -1609,9 +1874,13 @@ var VideoSDKCore = class {
1609
1874
  },
1610
1875
  onTrackStateChanged: (track) => {
1611
1876
  const pid = track.participant_id;
1612
- if (!pid || pid === this.myId) return;
1877
+ if (!pid || pid === this.myId) {
1878
+ return;
1879
+ }
1613
1880
  const current = this.state.getParticipant(pid);
1614
- if (!current) return;
1881
+ if (!current) {
1882
+ return;
1883
+ }
1615
1884
  if (track.source === "audio") {
1616
1885
  this.state.updateParticipantMedia(pid, {
1617
1886
  stream: current.media?.stream ?? null,
@@ -1627,17 +1896,22 @@ var VideoSDKCore = class {
1627
1896
  }
1628
1897
  this.events.onTrackStateChanged?.(track);
1629
1898
  },
1899
+ // RECORDING
1630
1900
  onRecordingStarted: (recording) => {
1631
1901
  this.events.onRecordingStarted?.(recording);
1632
1902
  },
1633
1903
  onRecordingStopped: (recording) => {
1634
1904
  this.events.onRecordingStopped?.(recording);
1635
1905
  },
1906
+ // REMOTE MEDIA
1636
1907
  onRemoteTrack: (track, stream, metadata) => {
1637
1908
  this.handleRemoteTrack(track, stream, metadata);
1638
1909
  },
1910
+ // SCREEN SHARE
1639
1911
  onScreenShareStarted: (participantId, track) => {
1640
- if (participantId === this.myId) return;
1912
+ if (participantId === this.myId) {
1913
+ return;
1914
+ }
1641
1915
  console.log("[SFU] Screen share started", participantId, track);
1642
1916
  const current = this.state.getParticipant(participantId);
1643
1917
  if (current) {
@@ -1648,7 +1922,9 @@ var VideoSDKCore = class {
1648
1922
  }
1649
1923
  },
1650
1924
  onScreenShareStopped: (participantId) => {
1651
- if (participantId === this.myId) return;
1925
+ if (participantId === this.myId) {
1926
+ return;
1927
+ }
1652
1928
  const screen = this.remoteScreenStreams.get(participantId);
1653
1929
  screen?.getTracks().forEach((track) => track.stop());
1654
1930
  this.remoteScreenStreams.delete(participantId);
@@ -1659,18 +1935,25 @@ var VideoSDKCore = class {
1659
1935
  });
1660
1936
  this.events.onScreenShareStopped?.(participantId);
1661
1937
  },
1938
+ // ERROR
1662
1939
  onError: (error) => {
1663
1940
  console.error("[SFU] Error:", error);
1664
- this.emitError("SFU_ERROR", error?.message || String(error), error, true);
1941
+ this.emitError(
1942
+ "SFU_ERROR",
1943
+ error?.message || String(error),
1944
+ error,
1945
+ true
1946
+ );
1665
1947
  }
1666
1948
  });
1667
1949
  this.sfuClient = client;
1668
1950
  client.setLocalStream(this.localStream);
1669
- return client.getToken(roomId, {
1951
+ await client.getToken(roomId, {
1670
1952
  displayName: name,
1671
1953
  userId: this.myId
1672
1954
  });
1673
1955
  }
1956
+ // SFU STATE
1674
1957
  handleSFUState(state) {
1675
1958
  this.events.onConnectionStateChanged?.(state);
1676
1959
  if (state === "reconnecting") {
@@ -1689,9 +1972,12 @@ var VideoSDKCore = class {
1689
1972
  );
1690
1973
  }
1691
1974
  }
1975
+ // REMOTE TRACKS
1692
1976
  handleRemoteTrack(track, stream, metadata) {
1693
1977
  const participantId = metadata?.participant_id;
1694
- if (!participantId || participantId === this.myId) return;
1978
+ if (!participantId || participantId === this.myId) {
1979
+ return;
1980
+ }
1695
1981
  const source = metadata.source;
1696
1982
  if (source === "screen") {
1697
1983
  let screenStream = this.remoteScreenStreams.get(participantId);
@@ -1736,39 +2022,47 @@ var VideoSDKCore = class {
1736
2022
  cameraTrack: track.kind === "video" ? track : current?.media?.cameraTrack,
1737
2023
  audioTrack: track.kind === "audio" ? track : current?.media?.audioTrack
1738
2024
  });
2025
+ if (track.kind === "audio") {
2026
+ this.activeSpeakerDetector.addTrack(participantId, track);
2027
+ }
1739
2028
  this.events.onTrack?.(remoteStream, participantId);
1740
2029
  track.onended = () => {
1741
2030
  if (track.kind === "video") {
1742
- const participant2 = this.state.getParticipant(participantId);
1743
2031
  this.state.updateParticipantMedia(participantId, {
1744
2032
  camEnabled: false,
1745
2033
  stream: remoteStream
1746
2034
  });
1747
2035
  return;
1748
2036
  }
1749
- const participant = this.state.getParticipant(participantId);
1750
2037
  this.state.updateParticipantMedia(participantId, {
1751
2038
  micEnabled: false,
1752
2039
  stream: remoteStream
1753
2040
  });
1754
2041
  };
1755
2042
  }
2043
+ // MEETING
1756
2044
  getMeeting() {
1757
2045
  return this.room;
1758
2046
  }
1759
- // ============ ADMISSION CONTROL ============
2047
+ // ADMISSION CONTROL
1760
2048
  async approveJoinRequest(requestId) {
1761
- if (!this.sfuClient) throw new Error("SFU client is not connected");
2049
+ if (!this.sfuClient) {
2050
+ throw new Error("SFU client is not connected");
2051
+ }
1762
2052
  return this.sfuClient.approveJoinRequest(requestId);
1763
2053
  }
1764
2054
  async rejectJoinRequest(requestId, reason) {
1765
- if (!this.sfuClient) throw new Error("SFU client is not connected");
2055
+ if (!this.sfuClient) {
2056
+ throw new Error("SFU client is not connected");
2057
+ }
1766
2058
  return this.sfuClient.rejectJoinRequest(requestId, reason);
1767
2059
  }
1768
- // ============ MEDIA CONTROL ============
2060
+ // MEDIA CONTROL
1769
2061
  toggleMic() {
1770
2062
  const mediaState = this.state.localParticipant?.media;
1771
- if (!mediaState || !this.localStream) return;
2063
+ if (!mediaState || !this.localStream) {
2064
+ return;
2065
+ }
1772
2066
  const nextEnabled = !mediaState.micEnabled;
1773
2067
  this.localStream.getAudioTracks().forEach((track) => {
1774
2068
  track.enabled = nextEnabled;
@@ -1776,14 +2070,18 @@ var VideoSDKCore = class {
1776
2070
  this.state.updateLocalParticipant({
1777
2071
  id: this.myId,
1778
2072
  name: this.participantName,
1779
- media: { micEnabled: nextEnabled }
2073
+ media: {
2074
+ micEnabled: nextEnabled
2075
+ }
1780
2076
  });
1781
2077
  this.sfuClient?.muteAudio(!nextEnabled);
1782
2078
  this.events.onMicToggled?.(this.myId, nextEnabled);
1783
2079
  }
1784
2080
  toggleCam() {
1785
2081
  const mediaState = this.state.localParticipant?.media;
1786
- if (!mediaState || !this.localStream) return;
2082
+ if (!mediaState || !this.localStream) {
2083
+ return;
2084
+ }
1787
2085
  const nextEnabled = !mediaState.camEnabled;
1788
2086
  this.localStream.getVideoTracks().forEach((track) => {
1789
2087
  track.enabled = nextEnabled;
@@ -1791,12 +2089,14 @@ var VideoSDKCore = class {
1791
2089
  this.state.updateLocalParticipant({
1792
2090
  id: this.myId,
1793
2091
  name: this.participantName,
1794
- media: { camEnabled: nextEnabled }
2092
+ media: {
2093
+ camEnabled: nextEnabled
2094
+ }
1795
2095
  });
1796
2096
  this.sfuClient?.muteVideo(!nextEnabled);
1797
2097
  this.events.onCamToggled?.(this.myId, nextEnabled);
1798
2098
  }
1799
- // ============ SCREEN SHARING ============
2099
+ // SCREEN SHARING
1800
2100
  async startScreenShare() {
1801
2101
  try {
1802
2102
  if (!navigator.mediaDevices?.getDisplayMedia) {
@@ -1828,7 +2128,9 @@ var VideoSDKCore = class {
1828
2128
  }
1829
2129
  }
1830
2130
  stopScreenShare() {
1831
- if (!this.screenStream) return;
2131
+ if (!this.screenStream) {
2132
+ return;
2133
+ }
1832
2134
  void this.sfuClient?.stopScreenShare();
1833
2135
  this.screenStream.getTracks().forEach((track) => track.stop());
1834
2136
  this.screenStream = null;
@@ -1841,7 +2143,7 @@ var VideoSDKCore = class {
1841
2143
  }
1842
2144
  });
1843
2145
  }
1844
- // ============ RECORDING ============
2146
+ // RECORDING
1845
2147
  async startRecording() {
1846
2148
  if (!this.sfuClient) {
1847
2149
  throw new Error("SFU client is not connected");
@@ -1860,7 +2162,7 @@ var VideoSDKCore = class {
1860
2162
  }
1861
2163
  return this.sfuClient.getRecordingStatus();
1862
2164
  }
1863
- // ============ CHAT ============
2165
+ // CHAT
1864
2166
  sendChatMessage(payload) {
1865
2167
  if (!this.sfuClient) {
1866
2168
  console.warn("SFU Client not connected");
@@ -1879,8 +2181,9 @@ var VideoSDKCore = class {
1879
2181
  this.state.addChatMessage(msg);
1880
2182
  this.events.onChatMessage?.(msg);
1881
2183
  }
1882
- // ============ CLEANUP ============
2184
+ // CLEANUP
1883
2185
  cleanupRemoteParticipant(participantId) {
2186
+ this.activeSpeakerDetector.removeParticipant(participantId);
1884
2187
  const stream = this.remoteStreams.get(participantId);
1885
2188
  stream?.getTracks().forEach((track) => track.stop());
1886
2189
  this.remoteStreams.delete(participantId);
@@ -1888,16 +2191,34 @@ var VideoSDKCore = class {
1888
2191
  screen?.getTracks().forEach((track) => track.stop());
1889
2192
  this.remoteScreenStreams.delete(participantId);
1890
2193
  }
2194
+ // DISCONNECT
1891
2195
  async disconnect() {
2196
+ if (this.disconnectPromise) {
2197
+ return this.disconnectPromise;
2198
+ }
2199
+ this.disconnectPromise = this.performDisconnect();
2200
+ try {
2201
+ await this.disconnectPromise;
2202
+ } finally {
2203
+ this.disconnectPromise = null;
2204
+ }
2205
+ }
2206
+ async performDisconnect() {
1892
2207
  this.intentionalDisconnect = true;
1893
- await this.sfuClient?.leave();
2208
+ this.joinPromise = null;
2209
+ const client = this.sfuClient;
1894
2210
  this.sfuClient = null;
2211
+ if (client) {
2212
+ await client.leave();
2213
+ }
1895
2214
  if (this.localStream) {
1896
2215
  this.localStream.getTracks().forEach((track) => track.stop());
1897
2216
  this.localStream = null;
1898
2217
  }
1899
- this.screenStream?.getTracks().forEach((track) => track.stop());
1900
- this.screenStream = null;
2218
+ if (this.screenStream) {
2219
+ this.screenStream.getTracks().forEach((track) => track.stop());
2220
+ this.screenStream = null;
2221
+ }
1901
2222
  this.isScreenSharing = false;
1902
2223
  for (const stream of this.remoteStreams.values()) {
1903
2224
  stream.getTracks().forEach((track) => track.stop());
@@ -1946,6 +2267,9 @@ var MeetingProvider = ({
1946
2267
  const trackStateListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
1947
2268
  const recordingStartedListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
1948
2269
  const recordingStoppedListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
2270
+ const activeSpeakerListeners = (0, import_react2.useRef)(
2271
+ /* @__PURE__ */ new Set()
2272
+ );
1949
2273
  if (!sdkRef.current) {
1950
2274
  sdkRef.current = new VideoSDKCore({
1951
2275
  onError: (err) => errorListeners.current.forEach((fn) => fn(err)),
@@ -1958,6 +2282,11 @@ var MeetingProvider = ({
1958
2282
  });
1959
2283
  }
1960
2284
  const sdk = sdkRef.current;
2285
+ const activeSpeakerIds = useMeetingStore(
2286
+ sdk.state,
2287
+ "activeSpeakers",
2288
+ (s) => Array.from(s.activeSpeakerIds)
2289
+ );
1961
2290
  const presenterId = useMeetingStore(
1962
2291
  sdk.state,
1963
2292
  "presenter",
@@ -2006,6 +2335,7 @@ var MeetingProvider = ({
2006
2335
  participants,
2007
2336
  messages,
2008
2337
  presenterId,
2338
+ activeSpeakerIds,
2009
2339
  usePubSub: (topic) => {
2010
2340
  if (topic !== "SECURE_CHAT") {
2011
2341
  throw new Error(`Unsupported PubSub argument: "${topic}"`);
@@ -2055,6 +2385,12 @@ var MeetingProvider = ({
2055
2385
  onRecordingStopped: (cb) => {
2056
2386
  recordingStoppedListeners.current.add(cb);
2057
2387
  return () => recordingStoppedListeners.current.delete(cb);
2388
+ },
2389
+ onActiveSpeakersChanged: (cb) => {
2390
+ activeSpeakerListeners.current.add(cb);
2391
+ return () => {
2392
+ activeSpeakerListeners.current.delete(cb);
2393
+ };
2058
2394
  }
2059
2395
  };
2060
2396
  }, [config, sdk, localParticipant, participants, messages, presenterId]);
@@ -2118,6 +2454,10 @@ var useMeeting = (handlers) => {
2118
2454
  if (!handlers?.onError) return;
2119
2455
  return ctx.onError(handlers.onError);
2120
2456
  }, [handlers?.onError]);
2457
+ (0, import_react4.useEffect)(() => {
2458
+ if (!handlers?.onActiveSpeakersChanged) return;
2459
+ return ctx.onActiveSpeakersChanged(handlers.onActiveSpeakersChanged);
2460
+ }, [handlers?.onActiveSpeakersChanged]);
2121
2461
  (0, import_react4.useEffect)(() => {
2122
2462
  if (!handlers?.onEntryRequested) return;
2123
2463
  return ctx.onEntryRequested(handlers.onEntryRequested);
@@ -2275,6 +2615,7 @@ function useMeetingPreview(roomId, userId) {
2275
2615
  }
2276
2616
  // Annotate the CommonJS export names for ESM import in node:
2277
2617
  0 && (module.exports = {
2618
+ ActiveSpeakerDetector,
2278
2619
  MeetingProvider,
2279
2620
  MeetingState,
2280
2621
  SFUClient,