@defcomm/meetingsdk 0.6.8 → 0.6.9
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.d.mts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +144 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +144 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -32,13 +32,16 @@ var ActiveSpeakerDetector = class {
|
|
|
32
32
|
this.timer = null;
|
|
33
33
|
this.lastEmitted = "";
|
|
34
34
|
this.intervalMs = options.intervalMs ?? 100;
|
|
35
|
-
this.threshold = options.threshold ?? 0.
|
|
36
|
-
this.releaseThreshold = options.releaseThreshold ?? 0.
|
|
37
|
-
this.releaseMs = options.releaseMs ??
|
|
35
|
+
this.threshold = options.threshold ?? 0.02;
|
|
36
|
+
this.releaseThreshold = options.releaseThreshold ?? 0.01;
|
|
37
|
+
this.releaseMs = options.releaseMs ?? 650;
|
|
38
38
|
this.onActiveSpeakersChanged = options.onActiveSpeakersChanged ?? (() => {
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
ensureAudioContext() {
|
|
42
|
+
if (typeof window === "undefined") {
|
|
43
|
+
throw new Error("AudioContext is only available in the browser");
|
|
44
|
+
}
|
|
42
45
|
if (!this.audioContext) {
|
|
43
46
|
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
44
47
|
if (!AudioContextClass) {
|
|
@@ -48,28 +51,52 @@ var ActiveSpeakerDetector = class {
|
|
|
48
51
|
}
|
|
49
52
|
return this.audioContext;
|
|
50
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Create/wake the Web Audio context. Call this from a user gesture when
|
|
56
|
+
* possible. Returns true when the context is running after the call.
|
|
57
|
+
*/
|
|
51
58
|
async resume() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
try {
|
|
60
|
+
const context = this.ensureAudioContext();
|
|
61
|
+
if (context.state === "suspended") {
|
|
62
|
+
try {
|
|
63
|
+
await context.resume();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn("[ActiveSpeaker] AudioContext resume failed", error);
|
|
66
|
+
}
|
|
57
67
|
}
|
|
68
|
+
return context.state === "running";
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.warn("[ActiveSpeaker] AudioContext unavailable", error);
|
|
71
|
+
return false;
|
|
58
72
|
}
|
|
59
73
|
}
|
|
74
|
+
getAudioContextState() {
|
|
75
|
+
if (!this.audioContext) return "unavailable";
|
|
76
|
+
return this.audioContext.state;
|
|
77
|
+
}
|
|
60
78
|
addTrack(participantId, track) {
|
|
61
|
-
if (!participantId) return;
|
|
62
|
-
if (track.kind !== "audio") return;
|
|
79
|
+
if (!participantId || track.kind !== "audio") return;
|
|
63
80
|
this.removeParticipant(participantId);
|
|
64
|
-
|
|
81
|
+
let context;
|
|
82
|
+
try {
|
|
83
|
+
context = this.ensureAudioContext();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn("[ActiveSpeaker] Could not create AudioContext", error);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
65
88
|
const stream = new MediaStream([track]);
|
|
66
89
|
const source = context.createMediaStreamSource(stream);
|
|
67
90
|
const analyser = context.createAnalyser();
|
|
68
91
|
analyser.fftSize = 512;
|
|
69
|
-
analyser.smoothingTimeConstant = 0.
|
|
92
|
+
analyser.smoothingTimeConstant = 0.35;
|
|
70
93
|
analyser.minDecibels = -90;
|
|
71
94
|
analyser.maxDecibels = -10;
|
|
72
95
|
source.connect(analyser);
|
|
96
|
+
const sink = context.createGain();
|
|
97
|
+
sink.gain.value = 0;
|
|
98
|
+
analyser.connect(sink);
|
|
99
|
+
sink.connect(context.destination);
|
|
73
100
|
const data = new Float32Array(analyser.fftSize);
|
|
74
101
|
const state = {
|
|
75
102
|
participantId,
|
|
@@ -77,15 +104,20 @@ var ActiveSpeakerDetector = class {
|
|
|
77
104
|
stream,
|
|
78
105
|
source,
|
|
79
106
|
analyser,
|
|
107
|
+
sink,
|
|
80
108
|
data,
|
|
81
109
|
active: false,
|
|
110
|
+
level: 0,
|
|
82
111
|
lastActiveAt: 0
|
|
83
112
|
};
|
|
84
113
|
this.speakers.set(participantId, state);
|
|
85
114
|
track.addEventListener(
|
|
86
115
|
"ended",
|
|
87
116
|
() => {
|
|
88
|
-
this.
|
|
117
|
+
const current = this.speakers.get(participantId);
|
|
118
|
+
if (current?.track === track) {
|
|
119
|
+
this.removeParticipant(participantId);
|
|
120
|
+
}
|
|
89
121
|
},
|
|
90
122
|
{ once: true }
|
|
91
123
|
);
|
|
@@ -96,15 +128,19 @@ var ActiveSpeakerDetector = class {
|
|
|
96
128
|
const state = this.speakers.get(participantId);
|
|
97
129
|
if (!state) return;
|
|
98
130
|
state.active = false;
|
|
131
|
+
state.level = 0;
|
|
99
132
|
try {
|
|
100
133
|
state.source.disconnect();
|
|
101
134
|
} catch {
|
|
102
135
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
136
|
+
try {
|
|
137
|
+
state.analyser.disconnect();
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
state.sink.disconnect();
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
108
144
|
this.speakers.delete(participantId);
|
|
109
145
|
this.emitIfChanged();
|
|
110
146
|
}
|
|
@@ -116,7 +152,7 @@ var ActiveSpeakerDetector = class {
|
|
|
116
152
|
}
|
|
117
153
|
}
|
|
118
154
|
clear() {
|
|
119
|
-
for (const participantId of this.speakers.keys()) {
|
|
155
|
+
for (const participantId of [...this.speakers.keys()]) {
|
|
120
156
|
this.removeParticipant(participantId);
|
|
121
157
|
}
|
|
122
158
|
this.speakers.clear();
|
|
@@ -136,6 +172,7 @@ var ActiveSpeakerDetector = class {
|
|
|
136
172
|
const now = performance.now();
|
|
137
173
|
for (const state of this.speakers.values()) {
|
|
138
174
|
if (state.track.readyState !== "live" || state.track.muted || !state.track.enabled) {
|
|
175
|
+
state.level = 0;
|
|
139
176
|
if (state.active) {
|
|
140
177
|
state.active = false;
|
|
141
178
|
}
|
|
@@ -148,30 +185,33 @@ var ActiveSpeakerDetector = class {
|
|
|
148
185
|
sumSquares += sample * sample;
|
|
149
186
|
}
|
|
150
187
|
const rms = Math.sqrt(sumSquares / state.data.length);
|
|
188
|
+
state.level = rms;
|
|
151
189
|
if (!state.active) {
|
|
152
190
|
if (rms >= this.threshold) {
|
|
153
191
|
state.active = true;
|
|
154
192
|
state.lastActiveAt = now;
|
|
155
193
|
}
|
|
156
|
-
} else {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
state.active = false;
|
|
161
|
-
}
|
|
194
|
+
} else if (rms >= this.releaseThreshold) {
|
|
195
|
+
state.lastActiveAt = now;
|
|
196
|
+
} else if (now - state.lastActiveAt >= this.releaseMs) {
|
|
197
|
+
state.active = false;
|
|
162
198
|
}
|
|
163
199
|
}
|
|
164
200
|
this.emitIfChanged();
|
|
165
201
|
}
|
|
202
|
+
getActiveSpeakers() {
|
|
203
|
+
return Array.from(this.speakers.values()).filter((speaker) => speaker.active).sort((a, b) => {
|
|
204
|
+
if (b.level !== a.level) return b.level - a.level;
|
|
205
|
+
return a.participantId.localeCompare(b.participantId);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
166
208
|
getActiveSpeakerIds() {
|
|
167
|
-
return
|
|
209
|
+
return this.getActiveSpeakers().map((speaker) => speaker.participantId);
|
|
168
210
|
}
|
|
169
211
|
emitIfChanged(force = false) {
|
|
170
|
-
const ids = this.getActiveSpeakerIds()
|
|
212
|
+
const ids = this.getActiveSpeakerIds();
|
|
171
213
|
const key = ids.join("|");
|
|
172
|
-
if (!force && key === this.lastEmitted)
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
214
|
+
if (!force && key === this.lastEmitted) return;
|
|
175
215
|
this.lastEmitted = key;
|
|
176
216
|
this.onActiveSpeakersChanged(ids);
|
|
177
217
|
}
|
|
@@ -2316,26 +2356,11 @@ var VideoSDKCore = class {
|
|
|
2316
2356
|
async initLocal(video, name) {
|
|
2317
2357
|
this.participantName = name;
|
|
2318
2358
|
try {
|
|
2319
|
-
this.localStream = await
|
|
2320
|
-
video: {
|
|
2321
|
-
width: { ideal: 1280 },
|
|
2322
|
-
height: { ideal: 720 }
|
|
2323
|
-
},
|
|
2324
|
-
audio: {
|
|
2325
|
-
echoCancellation: true,
|
|
2326
|
-
noiseSuppression: true,
|
|
2327
|
-
autoGainControl: true
|
|
2328
|
-
}
|
|
2329
|
-
});
|
|
2359
|
+
this.localStream = await this.acquireOptionalLocalMedia();
|
|
2330
2360
|
const localAudioTrack = this.localStream.getAudioTracks()[0];
|
|
2331
2361
|
if (localAudioTrack) {
|
|
2332
2362
|
this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
|
|
2333
2363
|
}
|
|
2334
|
-
const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
|
|
2335
|
-
const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
|
|
2336
|
-
if (!hasVideo || !hasAudio) {
|
|
2337
|
-
throw new Error(`Missing tracks: video=${hasVideo}, audio=${hasAudio}`);
|
|
2338
|
-
}
|
|
2339
2364
|
video.srcObject = this.localStream;
|
|
2340
2365
|
this.updateLocalMedia();
|
|
2341
2366
|
} catch (err) {
|
|
@@ -2352,8 +2377,8 @@ var VideoSDKCore = class {
|
|
|
2352
2377
|
stream: this.localStream,
|
|
2353
2378
|
cameraTrack: this.localStream.getVideoTracks()[0],
|
|
2354
2379
|
audioTrack: this.localStream.getAudioTracks()[0],
|
|
2355
|
-
micEnabled: this.localStream.getAudioTracks()[0]?.enabled ??
|
|
2356
|
-
camEnabled: this.localStream.getVideoTracks()[0]?.enabled ??
|
|
2380
|
+
micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? false,
|
|
2381
|
+
camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? false,
|
|
2357
2382
|
isScreenSharing: this.isScreenSharing,
|
|
2358
2383
|
screenStream: this.screenStream,
|
|
2359
2384
|
screenTrack: this.screenStream?.getVideoTracks()[0]
|
|
@@ -2380,6 +2405,70 @@ var VideoSDKCore = class {
|
|
|
2380
2405
|
async getMeetingSummary(roomCode, sessionId) {
|
|
2381
2406
|
return this.sfuClient.getMeetingSummary(roomCode, sessionId);
|
|
2382
2407
|
}
|
|
2408
|
+
async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId) {
|
|
2409
|
+
const combined = new MediaStream();
|
|
2410
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
2411
|
+
console.warn(
|
|
2412
|
+
"[SDK] Camera/microphone access is not supported; joining without local media."
|
|
2413
|
+
);
|
|
2414
|
+
return combined;
|
|
2415
|
+
}
|
|
2416
|
+
const cameraPromise = navigator.mediaDevices.getUserMedia({
|
|
2417
|
+
video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
|
|
2418
|
+
width: { ideal: 1280 },
|
|
2419
|
+
height: { ideal: 720 }
|
|
2420
|
+
},
|
|
2421
|
+
audio: false
|
|
2422
|
+
});
|
|
2423
|
+
const microphonePromise = navigator.mediaDevices.getUserMedia({
|
|
2424
|
+
video: false,
|
|
2425
|
+
audio: microphoneDeviceId ? {
|
|
2426
|
+
deviceId: { exact: microphoneDeviceId },
|
|
2427
|
+
echoCancellation: true,
|
|
2428
|
+
noiseSuppression: true,
|
|
2429
|
+
autoGainControl: true
|
|
2430
|
+
} : {
|
|
2431
|
+
echoCancellation: true,
|
|
2432
|
+
noiseSuppression: true,
|
|
2433
|
+
autoGainControl: true
|
|
2434
|
+
}
|
|
2435
|
+
});
|
|
2436
|
+
const [cameraResult, microphoneResult] = await Promise.allSettled([
|
|
2437
|
+
cameraPromise,
|
|
2438
|
+
microphonePromise
|
|
2439
|
+
]);
|
|
2440
|
+
if (cameraResult.status === "fulfilled") {
|
|
2441
|
+
for (const track of cameraResult.value.getVideoTracks()) {
|
|
2442
|
+
combined.addTrack(track);
|
|
2443
|
+
}
|
|
2444
|
+
} else {
|
|
2445
|
+
console.warn("[SDK] Camera unavailable; continuing without video.", {
|
|
2446
|
+
name: cameraResult.reason?.name,
|
|
2447
|
+
message: cameraResult.reason?.message
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
if (microphoneResult.status === "fulfilled") {
|
|
2451
|
+
for (const track of microphoneResult.value.getAudioTracks()) {
|
|
2452
|
+
combined.addTrack(track);
|
|
2453
|
+
}
|
|
2454
|
+
} else {
|
|
2455
|
+
console.warn(
|
|
2456
|
+
"[SDK] Microphone unavailable; continuing without audio.",
|
|
2457
|
+
{
|
|
2458
|
+
name: microphoneResult.reason?.name,
|
|
2459
|
+
message: microphoneResult.reason?.message
|
|
2460
|
+
}
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
return combined;
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
|
|
2467
|
+
* whenever possible (Join, first click inside the meeting, etc.).
|
|
2468
|
+
*/
|
|
2469
|
+
async resumeActiveSpeaker() {
|
|
2470
|
+
return this.activeSpeakerDetector.resume();
|
|
2471
|
+
}
|
|
2383
2472
|
// JOIN
|
|
2384
2473
|
async joinMeeting(config) {
|
|
2385
2474
|
const {
|
|
@@ -2423,15 +2512,10 @@ var VideoSDKCore = class {
|
|
|
2423
2512
|
this.room.id = roomId;
|
|
2424
2513
|
this.room.name = name;
|
|
2425
2514
|
if (!this.localStream) {
|
|
2426
|
-
this.localStream = await
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
echoCancellation: true,
|
|
2431
|
-
noiseSuppression: true,
|
|
2432
|
-
autoGainControl: true
|
|
2433
|
-
} : true
|
|
2434
|
-
});
|
|
2515
|
+
this.localStream = await this.acquireOptionalLocalMedia(
|
|
2516
|
+
cameraDeviceId,
|
|
2517
|
+
microphoneDeviceId
|
|
2518
|
+
);
|
|
2435
2519
|
const localAudioTrack = this.localStream.getAudioTracks()[0];
|
|
2436
2520
|
if (localAudioTrack) {
|
|
2437
2521
|
this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
|
|
@@ -2476,8 +2560,8 @@ var VideoSDKCore = class {
|
|
|
2476
2560
|
await this.sfuClient.getToken(roomId, {
|
|
2477
2561
|
displayName: name,
|
|
2478
2562
|
participantId: this.myId,
|
|
2479
|
-
audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ??
|
|
2480
|
-
videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ??
|
|
2563
|
+
audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? false),
|
|
2564
|
+
videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? false)
|
|
2481
2565
|
});
|
|
2482
2566
|
}
|
|
2483
2567
|
createSFUClient(token) {
|
|
@@ -3537,6 +3621,7 @@ var MeetingProvider = ({
|
|
|
3537
3621
|
getMeetingSummary: sdk.getMeetingSummary.bind(sdk),
|
|
3538
3622
|
toggleMic: sdk.toggleMic.bind(sdk),
|
|
3539
3623
|
toggleCam: sdk.toggleCam.bind(sdk),
|
|
3624
|
+
resumeActiveSpeaker: sdk.resumeActiveSpeaker.bind(sdk),
|
|
3540
3625
|
startScreenShare: sdk.startScreenShare.bind(sdk),
|
|
3541
3626
|
stopScreenShare: sdk.stopScreenShare.bind(sdk),
|
|
3542
3627
|
sendMessage: sdk.sendChatMessage.bind(sdk),
|