@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/README.md +984 -637
- package/dist/index.d.mts +53 -4
- package/dist/index.d.ts +53 -4
- package/dist/index.js +394 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +393 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
1473
|
-
this.
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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)
|
|
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)
|
|
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)
|
|
1813
|
+
if (participantId === this.myId) {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1560
1816
|
const current = this.state.getParticipant(participantId);
|
|
1561
|
-
if (!current)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
1877
|
+
if (!pid || pid === this.myId) {
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1613
1880
|
const current = this.state.getParticipant(pid);
|
|
1614
|
-
if (!current)
|
|
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)
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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
|
-
//
|
|
2047
|
+
// ADMISSION CONTROL
|
|
1760
2048
|
async approveJoinRequest(requestId) {
|
|
1761
|
-
if (!this.sfuClient)
|
|
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)
|
|
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
|
-
//
|
|
2060
|
+
// MEDIA CONTROL
|
|
1769
2061
|
toggleMic() {
|
|
1770
2062
|
const mediaState = this.state.localParticipant?.media;
|
|
1771
|
-
if (!mediaState || !this.localStream)
|
|
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: {
|
|
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)
|
|
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: {
|
|
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
|
-
//
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
1900
|
-
|
|
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,
|