@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.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.025;
36
- this.releaseThreshold = options.releaseThreshold ?? 0.015;
37
- this.releaseMs = options.releaseMs ?? 600;
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
- if (!this.audioContext) return;
53
- if (this.audioContext.state === "suspended") {
54
- try {
55
- await this.audioContext.resume();
56
- } catch {
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
- const context = this.ensureAudioContext();
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.65;
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.removeParticipant(participantId);
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
- state.stream.getTracks().forEach((track) => {
104
- if (track !== state.track) {
105
- track.stop();
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
- if (rms >= this.releaseThreshold) {
158
- state.lastActiveAt = now;
159
- } else if (now - state.lastActiveAt >= this.releaseMs) {
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 Array.from(this.speakers.values()).filter((speaker) => speaker.active).map((speaker) => speaker.participantId);
209
+ return this.getActiveSpeakers().map((speaker) => speaker.participantId);
168
210
  }
169
211
  emitIfChanged(force = false) {
170
- const ids = this.getActiveSpeakerIds().sort();
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 navigator.mediaDevices.getUserMedia({
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 ?? !audioMuted,
2356
- camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? !videoMuted,
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 navigator.mediaDevices.getUserMedia({
2427
- video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : true,
2428
- audio: microphoneDeviceId ? {
2429
- deviceId: { exact: microphoneDeviceId },
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 ?? true),
2480
- videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? true)
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),