@defcomm/meetingsdk 0.1.0 → 0.1.1

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
 
@@ -1313,6 +1482,12 @@ var VideoSDKCore = class {
1313
1482
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
1314
1483
  this.state = new MeetingState();
1315
1484
  this.events = events;
1485
+ this.activeSpeakerDetector = new ActiveSpeakerDetector({
1486
+ onActiveSpeakersChanged: (participantIds) => {
1487
+ this.state.setActiveSpeakers(participantIds);
1488
+ this.events.onActiveSpeakersChanged?.(participantIds);
1489
+ }
1490
+ });
1316
1491
  let storedId = null;
1317
1492
  try {
1318
1493
  storedId = typeof localStorage !== "undefined" ? localStorage.getItem("defcomm:participant_id") : null;
@@ -1353,6 +1528,10 @@ var VideoSDKCore = class {
1353
1528
  autoGainControl: true
1354
1529
  }
1355
1530
  });
1531
+ const localAudioTrack = this.localStream.getAudioTracks()[0];
1532
+ if (localAudioTrack) {
1533
+ this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
1534
+ }
1356
1535
  const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
1357
1536
  const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
1358
1537
  if (!hasVideo || !hasAudio) {
@@ -1409,10 +1588,19 @@ var VideoSDKCore = class {
1409
1588
  return client.deleteRoom(roomCode);
1410
1589
  }
1411
1590
  async joinMeeting(config) {
1412
- const { roomId, name, audioMuted = false, videoMuted = false } = config;
1591
+ const {
1592
+ roomId,
1593
+ name,
1594
+ audioMuted = false,
1595
+ videoMuted = false,
1596
+ userId
1597
+ } = config;
1413
1598
  if (!roomId || !name) {
1414
1599
  throw new Error("roomId and name are required to join meeting");
1415
1600
  }
1601
+ if (userId) {
1602
+ this.myId = userId;
1603
+ }
1416
1604
  this.intentionalDisconnect = false;
1417
1605
  this.participantName = name;
1418
1606
  this.room.id = roomId;
@@ -1422,6 +1610,10 @@ var VideoSDKCore = class {
1422
1610
  video: true,
1423
1611
  audio: true
1424
1612
  });
1613
+ const localAudioTrack = this.localStream.getAudioTracks()[0];
1614
+ if (localAudioTrack) {
1615
+ this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
1616
+ }
1425
1617
  }
1426
1618
  this.localStream.getAudioTracks().forEach((track) => {
1427
1619
  track.enabled = !audioMuted;
@@ -1444,10 +1636,7 @@ var VideoSDKCore = class {
1444
1636
  autoSubscribe: true,
1445
1637
  onStateChange: (state) => this.handleSFUState(state),
1446
1638
  onConnected: (payload, resumed) => {
1447
- console.log(
1448
- `[SFU] ${resumed ? "Resumed" : "Joined"} room`,
1449
- payload
1450
- );
1639
+ console.log(`[SFU] ${resumed ? "Resumed" : "Joined"} room`, payload);
1451
1640
  this.state.updateLocalParticipant({
1452
1641
  id: this.myId,
1453
1642
  name: this.participantName
@@ -1626,7 +1815,12 @@ var VideoSDKCore = class {
1626
1815
  },
1627
1816
  onError: (error) => {
1628
1817
  console.error("[SFU] Error:", error);
1629
- this.emitError("SFU_ERROR", error?.message || String(error), error, true);
1818
+ this.emitError(
1819
+ "SFU_ERROR",
1820
+ error?.message || String(error),
1821
+ error,
1822
+ true
1823
+ );
1630
1824
  }
1631
1825
  });
1632
1826
  this.sfuClient = client;
@@ -1701,6 +1895,9 @@ var VideoSDKCore = class {
1701
1895
  cameraTrack: track.kind === "video" ? track : current?.media?.cameraTrack,
1702
1896
  audioTrack: track.kind === "audio" ? track : current?.media?.audioTrack
1703
1897
  });
1898
+ if (track.kind === "audio") {
1899
+ this.activeSpeakerDetector.addTrack(participantId, track);
1900
+ }
1704
1901
  this.events.onTrack?.(remoteStream, participantId);
1705
1902
  track.onended = () => {
1706
1903
  if (track.kind === "video") {
@@ -1846,6 +2043,7 @@ var VideoSDKCore = class {
1846
2043
  }
1847
2044
  // ============ CLEANUP ============
1848
2045
  cleanupRemoteParticipant(participantId) {
2046
+ this.activeSpeakerDetector.removeParticipant(participantId);
1849
2047
  const stream = this.remoteStreams.get(participantId);
1850
2048
  stream?.getTracks().forEach((track) => track.stop());
1851
2049
  this.remoteStreams.delete(participantId);
@@ -1911,6 +2109,9 @@ var MeetingProvider = ({
1911
2109
  const trackStateListeners = useRef(/* @__PURE__ */ new Set());
1912
2110
  const recordingStartedListeners = useRef(/* @__PURE__ */ new Set());
1913
2111
  const recordingStoppedListeners = useRef(/* @__PURE__ */ new Set());
2112
+ const activeSpeakerListeners = useRef(
2113
+ /* @__PURE__ */ new Set()
2114
+ );
1914
2115
  if (!sdkRef.current) {
1915
2116
  sdkRef.current = new VideoSDKCore({
1916
2117
  onError: (err) => errorListeners.current.forEach((fn) => fn(err)),
@@ -1923,6 +2124,11 @@ var MeetingProvider = ({
1923
2124
  });
1924
2125
  }
1925
2126
  const sdk = sdkRef.current;
2127
+ const activeSpeakerIds = useMeetingStore(
2128
+ sdk.state,
2129
+ "activeSpeakers",
2130
+ (s) => Array.from(s.activeSpeakerIds)
2131
+ );
1926
2132
  const presenterId = useMeetingStore(
1927
2133
  sdk.state,
1928
2134
  "presenter",
@@ -1971,6 +2177,7 @@ var MeetingProvider = ({
1971
2177
  participants,
1972
2178
  messages,
1973
2179
  presenterId,
2180
+ activeSpeakerIds,
1974
2181
  usePubSub: (topic) => {
1975
2182
  if (topic !== "SECURE_CHAT") {
1976
2183
  throw new Error(`Unsupported PubSub argument: "${topic}"`);
@@ -2020,6 +2227,12 @@ var MeetingProvider = ({
2020
2227
  onRecordingStopped: (cb) => {
2021
2228
  recordingStoppedListeners.current.add(cb);
2022
2229
  return () => recordingStoppedListeners.current.delete(cb);
2230
+ },
2231
+ onActiveSpeakersChanged: (cb) => {
2232
+ activeSpeakerListeners.current.add(cb);
2233
+ return () => {
2234
+ activeSpeakerListeners.current.delete(cb);
2235
+ };
2023
2236
  }
2024
2237
  };
2025
2238
  }, [config, sdk, localParticipant, participants, messages, presenterId]);
@@ -2083,6 +2296,10 @@ var useMeeting = (handlers) => {
2083
2296
  if (!handlers?.onError) return;
2084
2297
  return ctx.onError(handlers.onError);
2085
2298
  }, [handlers?.onError]);
2299
+ useEffect3(() => {
2300
+ if (!handlers?.onActiveSpeakersChanged) return;
2301
+ return ctx.onActiveSpeakersChanged(handlers.onActiveSpeakersChanged);
2302
+ }, [handlers?.onActiveSpeakersChanged]);
2086
2303
  useEffect3(() => {
2087
2304
  if (!handlers?.onEntryRequested) return;
2088
2305
  return ctx.onEntryRequested(handlers.onEntryRequested);
@@ -2239,6 +2456,7 @@ function useMeetingPreview(roomId, userId) {
2239
2456
  };
2240
2457
  }
2241
2458
  export {
2459
+ ActiveSpeakerDetector,
2242
2460
  MeetingProvider,
2243
2461
  MeetingState,
2244
2462
  SFUClient,