@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.mjs CHANGED
@@ -6,9 +6,7 @@ import { createContext, useContext, useMemo, useRef } from "react";
6
6
 
7
7
  // src/config/ws.ts
8
8
  var SDK_CONFIG = {
9
- // SFU API endpoint for token generation
10
9
  apiBase: "https://sfu-server.duckdns.org/api",
11
- // SFU WebSocket endpoint for signaling
12
10
  wsBase: "wss://sfu-server.duckdns.org/ws",
13
11
  // Public STUN/TURN servers (fallback)
14
12
  iceServers: [
@@ -17,10 +15,170 @@ var SDK_CONFIG = {
17
15
  ],
18
16
  // Connection and reconnection settings
19
17
  connectionTimeout: 1e4,
20
- maxReconnectAttempts: 5,
21
- // Legacy WebSocket URL (if using the old mesh architecture)
22
- wsUrl: "wss://sfu-server.duckdns.org/ws"
23
- // kept for backward compatibility
18
+ maxReconnectAttempts: 5
19
+ };
20
+
21
+ // src/core/ActiveSpeakerDetector.ts
22
+ var ActiveSpeakerDetector = class {
23
+ constructor(options = {}) {
24
+ this.audioContext = null;
25
+ this.speakers = /* @__PURE__ */ new Map();
26
+ this.timer = null;
27
+ this.lastEmitted = "";
28
+ this.intervalMs = options.intervalMs ?? 100;
29
+ this.threshold = options.threshold ?? 0.025;
30
+ this.releaseThreshold = options.releaseThreshold ?? 0.015;
31
+ this.releaseMs = options.releaseMs ?? 600;
32
+ this.onActiveSpeakersChanged = options.onActiveSpeakersChanged ?? (() => {
33
+ });
34
+ }
35
+ ensureAudioContext() {
36
+ if (!this.audioContext) {
37
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
38
+ if (!AudioContextClass) {
39
+ throw new Error("Web Audio API is not supported by this browser");
40
+ }
41
+ this.audioContext = new AudioContextClass();
42
+ }
43
+ return this.audioContext;
44
+ }
45
+ async resume() {
46
+ if (!this.audioContext) return;
47
+ if (this.audioContext.state === "suspended") {
48
+ try {
49
+ await this.audioContext.resume();
50
+ } catch {
51
+ }
52
+ }
53
+ }
54
+ addTrack(participantId, track) {
55
+ if (!participantId) return;
56
+ if (track.kind !== "audio") return;
57
+ this.removeParticipant(participantId);
58
+ const context = this.ensureAudioContext();
59
+ const stream = new MediaStream([track]);
60
+ const source = context.createMediaStreamSource(stream);
61
+ const analyser = context.createAnalyser();
62
+ analyser.fftSize = 512;
63
+ analyser.smoothingTimeConstant = 0.65;
64
+ analyser.minDecibels = -90;
65
+ analyser.maxDecibels = -10;
66
+ source.connect(analyser);
67
+ const data = new Float32Array(analyser.fftSize);
68
+ const state = {
69
+ participantId,
70
+ track,
71
+ stream,
72
+ source,
73
+ analyser,
74
+ data,
75
+ active: false,
76
+ lastActiveAt: 0
77
+ };
78
+ this.speakers.set(participantId, state);
79
+ track.addEventListener(
80
+ "ended",
81
+ () => {
82
+ this.removeParticipant(participantId);
83
+ },
84
+ { once: true }
85
+ );
86
+ this.start();
87
+ void this.resume();
88
+ }
89
+ removeParticipant(participantId) {
90
+ const state = this.speakers.get(participantId);
91
+ if (!state) return;
92
+ state.active = false;
93
+ try {
94
+ state.source.disconnect();
95
+ } catch {
96
+ }
97
+ state.stream.getTracks().forEach((track) => {
98
+ if (track !== state.track) {
99
+ track.stop();
100
+ }
101
+ });
102
+ this.speakers.delete(participantId);
103
+ this.emitIfChanged();
104
+ }
105
+ removeTrack(track) {
106
+ for (const [participantId, state] of this.speakers) {
107
+ if (state.track === track) {
108
+ this.removeParticipant(participantId);
109
+ }
110
+ }
111
+ }
112
+ clear() {
113
+ for (const participantId of this.speakers.keys()) {
114
+ this.removeParticipant(participantId);
115
+ }
116
+ this.speakers.clear();
117
+ this.stop();
118
+ this.emitIfChanged(true);
119
+ }
120
+ start() {
121
+ if (this.timer) return;
122
+ this.timer = setInterval(() => this.sample(), this.intervalMs);
123
+ }
124
+ stop() {
125
+ if (!this.timer) return;
126
+ clearInterval(this.timer);
127
+ this.timer = null;
128
+ }
129
+ sample() {
130
+ const now = performance.now();
131
+ for (const state of this.speakers.values()) {
132
+ if (state.track.readyState !== "live" || state.track.muted || !state.track.enabled) {
133
+ if (state.active) {
134
+ state.active = false;
135
+ }
136
+ continue;
137
+ }
138
+ state.analyser.getFloatTimeDomainData(state.data);
139
+ let sumSquares = 0;
140
+ for (let i = 0; i < state.data.length; i++) {
141
+ const sample = state.data[i];
142
+ sumSquares += sample * sample;
143
+ }
144
+ const rms = Math.sqrt(sumSquares / state.data.length);
145
+ if (!state.active) {
146
+ if (rms >= this.threshold) {
147
+ state.active = true;
148
+ state.lastActiveAt = now;
149
+ }
150
+ } else {
151
+ if (rms >= this.releaseThreshold) {
152
+ state.lastActiveAt = now;
153
+ } else if (now - state.lastActiveAt >= this.releaseMs) {
154
+ state.active = false;
155
+ }
156
+ }
157
+ }
158
+ this.emitIfChanged();
159
+ }
160
+ getActiveSpeakerIds() {
161
+ return Array.from(this.speakers.values()).filter((speaker) => speaker.active).map((speaker) => speaker.participantId);
162
+ }
163
+ emitIfChanged(force = false) {
164
+ const ids = this.getActiveSpeakerIds().sort();
165
+ const key = ids.join("|");
166
+ if (!force && key === this.lastEmitted) {
167
+ return;
168
+ }
169
+ this.lastEmitted = key;
170
+ this.onActiveSpeakersChanged(ids);
171
+ }
172
+ async destroy() {
173
+ this.clear();
174
+ if (this.audioContext) {
175
+ try {
176
+ await this.audioContext.close();
177
+ } catch {
178
+ }
179
+ this.audioContext = null;
180
+ }
181
+ }
24
182
  };
25
183
 
26
184
  // src/core/MeetingState.ts
@@ -31,6 +189,7 @@ var MeetingState = class {
31
189
  this.localStream = null;
32
190
  this.chatMessages = /* @__PURE__ */ new Map();
33
191
  this.presenterId = null;
192
+ this.activeSpeakerIds = /* @__PURE__ */ new Set();
34
193
  this.listeners = /* @__PURE__ */ new Map();
35
194
  }
36
195
  // ---- reactive system ----
@@ -52,6 +211,14 @@ var MeetingState = class {
52
211
  this.notify("presenter");
53
212
  this.notify("participants");
54
213
  }
214
+ setActiveSpeakers(ids) {
215
+ const next = new Set(ids);
216
+ if (next.size === this.activeSpeakerIds.size && [...next].every((id) => this.activeSpeakerIds.has(id))) {
217
+ return;
218
+ }
219
+ this.activeSpeakerIds = next;
220
+ this.notify("activeSpeakers");
221
+ }
55
222
  // ---- participants ----
56
223
  addParticipant(p) {
57
224
  if (this.participants.has(p.id)) return false;
@@ -165,9 +332,11 @@ var MeetingState = class {
165
332
  this.participants.clear();
166
333
  this.chatMessages.clear();
167
334
  this.presenterId = null;
335
+ this.activeSpeakerIds = /* @__PURE__ */ new Set();
168
336
  this.notify("participants");
169
337
  this.notify("chat");
170
338
  this.notify("presenter");
339
+ this.notify("activeSpeakers");
171
340
  }
172
341
  };
173
342
 
@@ -1295,7 +1464,7 @@ var SFUClient = class {
1295
1464
 
1296
1465
  // src/core/VideoCore.ts
1297
1466
  var VideoSDKCore = class {
1298
- constructor(events = {}, apiBase = SDK_CONFIG.apiBase || "https://api.example.com/api", wsBase = SDK_CONFIG.wsBase || "wss://api.example.com/ws") {
1467
+ constructor(events = {}, apiBase = SDK_CONFIG.apiBase, wsBase = SDK_CONFIG.wsBase) {
1299
1468
  this.events = events;
1300
1469
  this.apiBase = apiBase;
1301
1470
  this.wsBase = wsBase;
@@ -1311,8 +1480,16 @@ var VideoSDKCore = class {
1311
1480
  this.intentionalDisconnect = false;
1312
1481
  this.remoteStreams = /* @__PURE__ */ new Map();
1313
1482
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
1483
+ this.joinPromise = null;
1484
+ this.disconnectPromise = null;
1314
1485
  this.state = new MeetingState();
1315
1486
  this.events = events;
1487
+ this.activeSpeakerDetector = new ActiveSpeakerDetector({
1488
+ onActiveSpeakersChanged: (participantIds) => {
1489
+ this.state.setActiveSpeakers(participantIds);
1490
+ this.events.onActiveSpeakersChanged?.(participantIds);
1491
+ }
1492
+ });
1316
1493
  let storedId = null;
1317
1494
  try {
1318
1495
  storedId = typeof localStorage !== "undefined" ? localStorage.getItem("defcomm:participant_id") : null;
@@ -1338,7 +1515,7 @@ var VideoSDKCore = class {
1338
1515
  this.events.onError?.(err);
1339
1516
  console.error("[MeetingSDK Error]", err);
1340
1517
  }
1341
- // ============ LOCAL MEDIA ============
1518
+ // LOCAL MEDIA
1342
1519
  async initLocal(video, name) {
1343
1520
  this.participantName = name;
1344
1521
  try {
@@ -1353,6 +1530,10 @@ var VideoSDKCore = class {
1353
1530
  autoGainControl: true
1354
1531
  }
1355
1532
  });
1533
+ const localAudioTrack = this.localStream.getAudioTracks()[0];
1534
+ if (localAudioTrack) {
1535
+ this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
1536
+ }
1356
1537
  const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
1357
1538
  const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
1358
1539
  if (!hasVideo || !hasAudio) {
@@ -1383,7 +1564,7 @@ var VideoSDKCore = class {
1383
1564
  });
1384
1565
  this.state.localStream = this.localStream;
1385
1566
  }
1386
- // ============ CONNECTION / SIGNALLING ============
1567
+ // ROOM MANAGEMENT
1387
1568
  async createRoom(input) {
1388
1569
  const client = this.sfuClient ?? new SFUClient({
1389
1570
  apiBase: this.apiBase,
@@ -1408,11 +1589,43 @@ var VideoSDKCore = class {
1408
1589
  });
1409
1590
  return client.deleteRoom(roomCode);
1410
1591
  }
1592
+ // JOIN
1411
1593
  async joinMeeting(config) {
1412
- const { roomId, name, audioMuted = false, videoMuted = false } = config;
1594
+ const {
1595
+ roomId,
1596
+ name,
1597
+ audioMuted = false,
1598
+ videoMuted = false,
1599
+ userId
1600
+ } = config;
1413
1601
  if (!roomId || !name) {
1414
1602
  throw new Error("roomId and name are required to join meeting");
1415
1603
  }
1604
+ if (userId) {
1605
+ this.myId = userId;
1606
+ }
1607
+ if (this.sfuClient && this.room.id === roomId && this.sfuClient.getConnectionState() === "connected") {
1608
+ console.info("[SDK] Already connected; duplicate join ignored", {
1609
+ roomId,
1610
+ userId: this.myId
1611
+ });
1612
+ return;
1613
+ }
1614
+ if (this.joinPromise) {
1615
+ console.info(
1616
+ "[SDK] Join already in progress; waiting for existing join",
1617
+ {
1618
+ roomId,
1619
+ userId: this.myId
1620
+ }
1621
+ );
1622
+ return this.joinPromise;
1623
+ }
1624
+ if (this.sfuClient && this.room.id && this.room.id !== roomId) {
1625
+ throw new Error(
1626
+ `Already connected to room ${this.room.id}. Leave the current meeting before joining another room.`
1627
+ );
1628
+ }
1416
1629
  this.intentionalDisconnect = false;
1417
1630
  this.participantName = name;
1418
1631
  this.room.id = roomId;
@@ -1422,6 +1635,10 @@ var VideoSDKCore = class {
1422
1635
  video: true,
1423
1636
  audio: true
1424
1637
  });
1638
+ const localAudioTrack = this.localStream.getAudioTracks()[0];
1639
+ if (localAudioTrack) {
1640
+ this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
1641
+ }
1425
1642
  }
1426
1643
  this.localStream.getAudioTracks().forEach((track) => {
1427
1644
  track.enabled = !audioMuted;
@@ -1430,24 +1647,45 @@ var VideoSDKCore = class {
1430
1647
  track.enabled = !videoMuted;
1431
1648
  });
1432
1649
  this.updateLocalMedia(audioMuted, videoMuted);
1433
- return this.connect(roomId, name);
1650
+ const promise = this.connect(roomId, name);
1651
+ this.joinPromise = promise;
1652
+ try {
1653
+ await promise;
1654
+ } finally {
1655
+ if (this.joinPromise === promise) {
1656
+ this.joinPromise = null;
1657
+ }
1658
+ }
1434
1659
  }
1660
+ // SFU CONNECTION
1435
1661
  async connect(roomId, name) {
1436
1662
  if (this.sfuClient) {
1437
- await this.sfuClient.leave();
1438
- this.sfuClient = null;
1663
+ const state = this.sfuClient.getConnectionState();
1664
+ if (this.room.id === roomId && (state === "connecting" || state === "connected" || state === "reconnecting")) {
1665
+ console.info(
1666
+ "[SDK] Existing SFU connection is active; connect ignored",
1667
+ {
1668
+ roomId,
1669
+ state
1670
+ }
1671
+ );
1672
+ return;
1673
+ }
1674
+ throw new Error(
1675
+ `SFU client already exists in state "${state}". Disconnect explicitly before creating another connection.`
1676
+ );
1439
1677
  }
1440
1678
  const client = new SFUClient({
1441
1679
  apiBase: this.apiBase,
1442
1680
  wsBase: this.wsBase,
1443
1681
  userId: this.myId,
1444
1682
  autoSubscribe: true,
1445
- onStateChange: (state) => this.handleSFUState(state),
1683
+ // CONNECTION STATE
1684
+ onStateChange: (state) => {
1685
+ this.handleSFUState(state);
1686
+ },
1446
1687
  onConnected: (payload, resumed) => {
1447
- console.log(
1448
- `[SFU] ${resumed ? "Resumed" : "Joined"} room`,
1449
- payload
1450
- );
1688
+ console.log(`[SFU] ${resumed ? "Resumed" : "Joined"} room`, payload);
1451
1689
  this.state.updateLocalParticipant({
1452
1690
  id: this.myId,
1453
1691
  name: this.participantName
@@ -1459,6 +1697,7 @@ var VideoSDKCore = class {
1459
1697
  onReconnecting: (attempt) => {
1460
1698
  console.warn(`[SFU] Reconnecting, attempt ${attempt}`);
1461
1699
  },
1700
+ // ADMISSION CONTROL
1462
1701
  onJoinPending: (info) => {
1463
1702
  console.log("[SFU] Join request pending", info);
1464
1703
  this.events.onEntryRequested?.({
@@ -1471,7 +1710,10 @@ var VideoSDKCore = class {
1471
1710
  console.log("[SFU] Join approved", requestId);
1472
1711
  this.events.onJoinApproved?.(requestId);
1473
1712
  this.events.onEntryResponded?.(
1474
- { participantId: this.myId, decision: "approved" },
1713
+ {
1714
+ participantId: this.myId,
1715
+ decision: "approved"
1716
+ },
1475
1717
  "approved"
1476
1718
  );
1477
1719
  },
@@ -1479,13 +1721,19 @@ var VideoSDKCore = class {
1479
1721
  console.warn("[SFU] Join rejected", requestId, reason);
1480
1722
  this.events.onJoinRejected?.(requestId);
1481
1723
  this.events.onEntryResponded?.(
1482
- { participantId: this.myId, decision: "rejected" },
1724
+ {
1725
+ participantId: this.myId,
1726
+ decision: "rejected"
1727
+ },
1483
1728
  "rejected"
1484
1729
  );
1485
1730
  this.emitError(
1486
1731
  "JOIN_REJECTED",
1487
1732
  reason || "Your request to join was rejected",
1488
- { requestId, reason },
1733
+ {
1734
+ requestId,
1735
+ reason
1736
+ },
1489
1737
  false
1490
1738
  );
1491
1739
  },
@@ -1493,16 +1741,21 @@ var VideoSDKCore = class {
1493
1741
  const requestId = request?.request_id || request?.requestId;
1494
1742
  const userId = request?.user_id || request?.userId;
1495
1743
  const nameValue = request?.user_metadata?.name || request?.display_name || request?.name || "Participant";
1496
- if (!requestId || !userId) return;
1744
+ if (!requestId || !userId) {
1745
+ return;
1746
+ }
1497
1747
  this.events.onEntryRequested?.({
1498
1748
  requestId,
1499
1749
  userId,
1500
1750
  name: nameValue
1501
1751
  });
1502
1752
  },
1753
+ // PARTICIPANTS
1503
1754
  onParticipantJoined: (participant) => {
1504
1755
  const pid = participant.participant_id;
1505
- if (!pid || pid === this.myId) return;
1756
+ if (!pid || pid === this.myId) {
1757
+ return;
1758
+ }
1506
1759
  const pname = participant.user_metadata?.name || participant.display_name || "Participant";
1507
1760
  this.state.addParticipant({
1508
1761
  id: pid,
@@ -1521,9 +1774,13 @@ var VideoSDKCore = class {
1521
1774
  });
1522
1775
  },
1523
1776
  onParticipantPresence: (participantId, presence) => {
1524
- if (participantId === this.myId) return;
1777
+ if (participantId === this.myId) {
1778
+ return;
1779
+ }
1525
1780
  const current = this.state.getParticipant(participantId);
1526
- if (!current) return;
1781
+ if (!current) {
1782
+ return;
1783
+ }
1527
1784
  const connectionState = presence === "reconnecting" ? "reconnecting" : presence === "connected" || presence === "online" ? "connected" : presence === "disconnected" ? "disconnected" : current.connectionState;
1528
1785
  this.state.updateParticipantMedia(participantId, {
1529
1786
  stream: current.media?.stream ?? null
@@ -1539,19 +1796,24 @@ var VideoSDKCore = class {
1539
1796
  },
1540
1797
  onParticipantLeft: (participant) => {
1541
1798
  const pid = typeof participant === "string" ? participant : participant.participant_id;
1542
- if (!pid) return;
1799
+ if (!pid) {
1800
+ return;
1801
+ }
1543
1802
  console.log("[SFU] Participant permanently left", pid);
1544
1803
  this.cleanupRemoteParticipant(pid);
1545
1804
  this.state.removeParticipant(pid);
1546
1805
  this.events.onUserLeft?.(pid);
1547
1806
  },
1807
+ // TRACKS
1548
1808
  onTrackPublished: (track) => {
1549
1809
  console.log("[SFU] Track published", track);
1550
1810
  },
1551
1811
  onTrackUnpublished: (track) => {
1552
1812
  console.log("[SFU] Track unpublished", track);
1553
1813
  const pid = track.participant_id;
1554
- if (!pid) return;
1814
+ if (!pid) {
1815
+ return;
1816
+ }
1555
1817
  const current = this.state.getParticipant(pid);
1556
1818
  if (track.source === "screen") {
1557
1819
  const screen = this.remoteScreenStreams.get(pid);
@@ -1565,7 +1827,9 @@ var VideoSDKCore = class {
1565
1827
  this.events.onScreenShareStopped?.(pid);
1566
1828
  return;
1567
1829
  }
1568
- if (!current) return;
1830
+ if (!current) {
1831
+ return;
1832
+ }
1569
1833
  this.state.updateParticipantMedia(pid, {
1570
1834
  stream: current.media?.stream ?? null,
1571
1835
  micEnabled: track.kind === "audio" ? false : current.media?.micEnabled ?? true,
@@ -1574,9 +1838,13 @@ var VideoSDKCore = class {
1574
1838
  },
1575
1839
  onTrackStateChanged: (track) => {
1576
1840
  const pid = track.participant_id;
1577
- if (!pid || pid === this.myId) return;
1841
+ if (!pid || pid === this.myId) {
1842
+ return;
1843
+ }
1578
1844
  const current = this.state.getParticipant(pid);
1579
- if (!current) return;
1845
+ if (!current) {
1846
+ return;
1847
+ }
1580
1848
  if (track.source === "audio") {
1581
1849
  this.state.updateParticipantMedia(pid, {
1582
1850
  stream: current.media?.stream ?? null,
@@ -1592,17 +1860,22 @@ var VideoSDKCore = class {
1592
1860
  }
1593
1861
  this.events.onTrackStateChanged?.(track);
1594
1862
  },
1863
+ // RECORDING
1595
1864
  onRecordingStarted: (recording) => {
1596
1865
  this.events.onRecordingStarted?.(recording);
1597
1866
  },
1598
1867
  onRecordingStopped: (recording) => {
1599
1868
  this.events.onRecordingStopped?.(recording);
1600
1869
  },
1870
+ // REMOTE MEDIA
1601
1871
  onRemoteTrack: (track, stream, metadata) => {
1602
1872
  this.handleRemoteTrack(track, stream, metadata);
1603
1873
  },
1874
+ // SCREEN SHARE
1604
1875
  onScreenShareStarted: (participantId, track) => {
1605
- if (participantId === this.myId) return;
1876
+ if (participantId === this.myId) {
1877
+ return;
1878
+ }
1606
1879
  console.log("[SFU] Screen share started", participantId, track);
1607
1880
  const current = this.state.getParticipant(participantId);
1608
1881
  if (current) {
@@ -1613,7 +1886,9 @@ var VideoSDKCore = class {
1613
1886
  }
1614
1887
  },
1615
1888
  onScreenShareStopped: (participantId) => {
1616
- if (participantId === this.myId) return;
1889
+ if (participantId === this.myId) {
1890
+ return;
1891
+ }
1617
1892
  const screen = this.remoteScreenStreams.get(participantId);
1618
1893
  screen?.getTracks().forEach((track) => track.stop());
1619
1894
  this.remoteScreenStreams.delete(participantId);
@@ -1624,18 +1899,25 @@ var VideoSDKCore = class {
1624
1899
  });
1625
1900
  this.events.onScreenShareStopped?.(participantId);
1626
1901
  },
1902
+ // ERROR
1627
1903
  onError: (error) => {
1628
1904
  console.error("[SFU] Error:", error);
1629
- this.emitError("SFU_ERROR", error?.message || String(error), error, true);
1905
+ this.emitError(
1906
+ "SFU_ERROR",
1907
+ error?.message || String(error),
1908
+ error,
1909
+ true
1910
+ );
1630
1911
  }
1631
1912
  });
1632
1913
  this.sfuClient = client;
1633
1914
  client.setLocalStream(this.localStream);
1634
- return client.getToken(roomId, {
1915
+ await client.getToken(roomId, {
1635
1916
  displayName: name,
1636
1917
  userId: this.myId
1637
1918
  });
1638
1919
  }
1920
+ // SFU STATE
1639
1921
  handleSFUState(state) {
1640
1922
  this.events.onConnectionStateChanged?.(state);
1641
1923
  if (state === "reconnecting") {
@@ -1654,9 +1936,12 @@ var VideoSDKCore = class {
1654
1936
  );
1655
1937
  }
1656
1938
  }
1939
+ // REMOTE TRACKS
1657
1940
  handleRemoteTrack(track, stream, metadata) {
1658
1941
  const participantId = metadata?.participant_id;
1659
- if (!participantId || participantId === this.myId) return;
1942
+ if (!participantId || participantId === this.myId) {
1943
+ return;
1944
+ }
1660
1945
  const source = metadata.source;
1661
1946
  if (source === "screen") {
1662
1947
  let screenStream = this.remoteScreenStreams.get(participantId);
@@ -1701,39 +1986,47 @@ var VideoSDKCore = class {
1701
1986
  cameraTrack: track.kind === "video" ? track : current?.media?.cameraTrack,
1702
1987
  audioTrack: track.kind === "audio" ? track : current?.media?.audioTrack
1703
1988
  });
1989
+ if (track.kind === "audio") {
1990
+ this.activeSpeakerDetector.addTrack(participantId, track);
1991
+ }
1704
1992
  this.events.onTrack?.(remoteStream, participantId);
1705
1993
  track.onended = () => {
1706
1994
  if (track.kind === "video") {
1707
- const participant2 = this.state.getParticipant(participantId);
1708
1995
  this.state.updateParticipantMedia(participantId, {
1709
1996
  camEnabled: false,
1710
1997
  stream: remoteStream
1711
1998
  });
1712
1999
  return;
1713
2000
  }
1714
- const participant = this.state.getParticipant(participantId);
1715
2001
  this.state.updateParticipantMedia(participantId, {
1716
2002
  micEnabled: false,
1717
2003
  stream: remoteStream
1718
2004
  });
1719
2005
  };
1720
2006
  }
2007
+ // MEETING
1721
2008
  getMeeting() {
1722
2009
  return this.room;
1723
2010
  }
1724
- // ============ ADMISSION CONTROL ============
2011
+ // ADMISSION CONTROL
1725
2012
  async approveJoinRequest(requestId) {
1726
- if (!this.sfuClient) throw new Error("SFU client is not connected");
2013
+ if (!this.sfuClient) {
2014
+ throw new Error("SFU client is not connected");
2015
+ }
1727
2016
  return this.sfuClient.approveJoinRequest(requestId);
1728
2017
  }
1729
2018
  async rejectJoinRequest(requestId, reason) {
1730
- if (!this.sfuClient) throw new Error("SFU client is not connected");
2019
+ if (!this.sfuClient) {
2020
+ throw new Error("SFU client is not connected");
2021
+ }
1731
2022
  return this.sfuClient.rejectJoinRequest(requestId, reason);
1732
2023
  }
1733
- // ============ MEDIA CONTROL ============
2024
+ // MEDIA CONTROL
1734
2025
  toggleMic() {
1735
2026
  const mediaState = this.state.localParticipant?.media;
1736
- if (!mediaState || !this.localStream) return;
2027
+ if (!mediaState || !this.localStream) {
2028
+ return;
2029
+ }
1737
2030
  const nextEnabled = !mediaState.micEnabled;
1738
2031
  this.localStream.getAudioTracks().forEach((track) => {
1739
2032
  track.enabled = nextEnabled;
@@ -1741,14 +2034,18 @@ var VideoSDKCore = class {
1741
2034
  this.state.updateLocalParticipant({
1742
2035
  id: this.myId,
1743
2036
  name: this.participantName,
1744
- media: { micEnabled: nextEnabled }
2037
+ media: {
2038
+ micEnabled: nextEnabled
2039
+ }
1745
2040
  });
1746
2041
  this.sfuClient?.muteAudio(!nextEnabled);
1747
2042
  this.events.onMicToggled?.(this.myId, nextEnabled);
1748
2043
  }
1749
2044
  toggleCam() {
1750
2045
  const mediaState = this.state.localParticipant?.media;
1751
- if (!mediaState || !this.localStream) return;
2046
+ if (!mediaState || !this.localStream) {
2047
+ return;
2048
+ }
1752
2049
  const nextEnabled = !mediaState.camEnabled;
1753
2050
  this.localStream.getVideoTracks().forEach((track) => {
1754
2051
  track.enabled = nextEnabled;
@@ -1756,12 +2053,14 @@ var VideoSDKCore = class {
1756
2053
  this.state.updateLocalParticipant({
1757
2054
  id: this.myId,
1758
2055
  name: this.participantName,
1759
- media: { camEnabled: nextEnabled }
2056
+ media: {
2057
+ camEnabled: nextEnabled
2058
+ }
1760
2059
  });
1761
2060
  this.sfuClient?.muteVideo(!nextEnabled);
1762
2061
  this.events.onCamToggled?.(this.myId, nextEnabled);
1763
2062
  }
1764
- // ============ SCREEN SHARING ============
2063
+ // SCREEN SHARING
1765
2064
  async startScreenShare() {
1766
2065
  try {
1767
2066
  if (!navigator.mediaDevices?.getDisplayMedia) {
@@ -1793,7 +2092,9 @@ var VideoSDKCore = class {
1793
2092
  }
1794
2093
  }
1795
2094
  stopScreenShare() {
1796
- if (!this.screenStream) return;
2095
+ if (!this.screenStream) {
2096
+ return;
2097
+ }
1797
2098
  void this.sfuClient?.stopScreenShare();
1798
2099
  this.screenStream.getTracks().forEach((track) => track.stop());
1799
2100
  this.screenStream = null;
@@ -1806,7 +2107,7 @@ var VideoSDKCore = class {
1806
2107
  }
1807
2108
  });
1808
2109
  }
1809
- // ============ RECORDING ============
2110
+ // RECORDING
1810
2111
  async startRecording() {
1811
2112
  if (!this.sfuClient) {
1812
2113
  throw new Error("SFU client is not connected");
@@ -1825,7 +2126,7 @@ var VideoSDKCore = class {
1825
2126
  }
1826
2127
  return this.sfuClient.getRecordingStatus();
1827
2128
  }
1828
- // ============ CHAT ============
2129
+ // CHAT
1829
2130
  sendChatMessage(payload) {
1830
2131
  if (!this.sfuClient) {
1831
2132
  console.warn("SFU Client not connected");
@@ -1844,8 +2145,9 @@ var VideoSDKCore = class {
1844
2145
  this.state.addChatMessage(msg);
1845
2146
  this.events.onChatMessage?.(msg);
1846
2147
  }
1847
- // ============ CLEANUP ============
2148
+ // CLEANUP
1848
2149
  cleanupRemoteParticipant(participantId) {
2150
+ this.activeSpeakerDetector.removeParticipant(participantId);
1849
2151
  const stream = this.remoteStreams.get(participantId);
1850
2152
  stream?.getTracks().forEach((track) => track.stop());
1851
2153
  this.remoteStreams.delete(participantId);
@@ -1853,16 +2155,34 @@ var VideoSDKCore = class {
1853
2155
  screen?.getTracks().forEach((track) => track.stop());
1854
2156
  this.remoteScreenStreams.delete(participantId);
1855
2157
  }
2158
+ // DISCONNECT
1856
2159
  async disconnect() {
2160
+ if (this.disconnectPromise) {
2161
+ return this.disconnectPromise;
2162
+ }
2163
+ this.disconnectPromise = this.performDisconnect();
2164
+ try {
2165
+ await this.disconnectPromise;
2166
+ } finally {
2167
+ this.disconnectPromise = null;
2168
+ }
2169
+ }
2170
+ async performDisconnect() {
1857
2171
  this.intentionalDisconnect = true;
1858
- await this.sfuClient?.leave();
2172
+ this.joinPromise = null;
2173
+ const client = this.sfuClient;
1859
2174
  this.sfuClient = null;
2175
+ if (client) {
2176
+ await client.leave();
2177
+ }
1860
2178
  if (this.localStream) {
1861
2179
  this.localStream.getTracks().forEach((track) => track.stop());
1862
2180
  this.localStream = null;
1863
2181
  }
1864
- this.screenStream?.getTracks().forEach((track) => track.stop());
1865
- this.screenStream = null;
2182
+ if (this.screenStream) {
2183
+ this.screenStream.getTracks().forEach((track) => track.stop());
2184
+ this.screenStream = null;
2185
+ }
1866
2186
  this.isScreenSharing = false;
1867
2187
  for (const stream of this.remoteStreams.values()) {
1868
2188
  stream.getTracks().forEach((track) => track.stop());
@@ -1911,6 +2231,9 @@ var MeetingProvider = ({
1911
2231
  const trackStateListeners = useRef(/* @__PURE__ */ new Set());
1912
2232
  const recordingStartedListeners = useRef(/* @__PURE__ */ new Set());
1913
2233
  const recordingStoppedListeners = useRef(/* @__PURE__ */ new Set());
2234
+ const activeSpeakerListeners = useRef(
2235
+ /* @__PURE__ */ new Set()
2236
+ );
1914
2237
  if (!sdkRef.current) {
1915
2238
  sdkRef.current = new VideoSDKCore({
1916
2239
  onError: (err) => errorListeners.current.forEach((fn) => fn(err)),
@@ -1923,6 +2246,11 @@ var MeetingProvider = ({
1923
2246
  });
1924
2247
  }
1925
2248
  const sdk = sdkRef.current;
2249
+ const activeSpeakerIds = useMeetingStore(
2250
+ sdk.state,
2251
+ "activeSpeakers",
2252
+ (s) => Array.from(s.activeSpeakerIds)
2253
+ );
1926
2254
  const presenterId = useMeetingStore(
1927
2255
  sdk.state,
1928
2256
  "presenter",
@@ -1971,6 +2299,7 @@ var MeetingProvider = ({
1971
2299
  participants,
1972
2300
  messages,
1973
2301
  presenterId,
2302
+ activeSpeakerIds,
1974
2303
  usePubSub: (topic) => {
1975
2304
  if (topic !== "SECURE_CHAT") {
1976
2305
  throw new Error(`Unsupported PubSub argument: "${topic}"`);
@@ -2020,6 +2349,12 @@ var MeetingProvider = ({
2020
2349
  onRecordingStopped: (cb) => {
2021
2350
  recordingStoppedListeners.current.add(cb);
2022
2351
  return () => recordingStoppedListeners.current.delete(cb);
2352
+ },
2353
+ onActiveSpeakersChanged: (cb) => {
2354
+ activeSpeakerListeners.current.add(cb);
2355
+ return () => {
2356
+ activeSpeakerListeners.current.delete(cb);
2357
+ };
2023
2358
  }
2024
2359
  };
2025
2360
  }, [config, sdk, localParticipant, participants, messages, presenterId]);
@@ -2083,6 +2418,10 @@ var useMeeting = (handlers) => {
2083
2418
  if (!handlers?.onError) return;
2084
2419
  return ctx.onError(handlers.onError);
2085
2420
  }, [handlers?.onError]);
2421
+ useEffect3(() => {
2422
+ if (!handlers?.onActiveSpeakersChanged) return;
2423
+ return ctx.onActiveSpeakersChanged(handlers.onActiveSpeakersChanged);
2424
+ }, [handlers?.onActiveSpeakersChanged]);
2086
2425
  useEffect3(() => {
2087
2426
  if (!handlers?.onEntryRequested) return;
2088
2427
  return ctx.onEntryRequested(handlers.onEntryRequested);
@@ -2239,6 +2578,7 @@ function useMeetingPreview(roomId, userId) {
2239
2578
  };
2240
2579
  }
2241
2580
  export {
2581
+ ActiveSpeakerDetector,
2242
2582
  MeetingProvider,
2243
2583
  MeetingState,
2244
2584
  SFUClient,