@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/README.md +984 -637
- package/dist/index.d.mts +50 -4
- package/dist/index.d.ts +50 -4
- package/dist/index.js +231 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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(
|
|
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,
|