@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.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
|
|
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
1438
|
-
this.
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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)
|
|
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)
|
|
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)
|
|
1777
|
+
if (participantId === this.myId) {
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1525
1780
|
const current = this.state.getParticipant(participantId);
|
|
1526
|
-
if (!current)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
1841
|
+
if (!pid || pid === this.myId) {
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1578
1844
|
const current = this.state.getParticipant(pid);
|
|
1579
|
-
if (!current)
|
|
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)
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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
|
-
//
|
|
2011
|
+
// ADMISSION CONTROL
|
|
1725
2012
|
async approveJoinRequest(requestId) {
|
|
1726
|
-
if (!this.sfuClient)
|
|
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)
|
|
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
|
-
//
|
|
2024
|
+
// MEDIA CONTROL
|
|
1734
2025
|
toggleMic() {
|
|
1735
2026
|
const mediaState = this.state.localParticipant?.media;
|
|
1736
|
-
if (!mediaState || !this.localStream)
|
|
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: {
|
|
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)
|
|
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: {
|
|
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
|
-
//
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
1865
|
-
|
|
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,
|