@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 CHANGED
@@ -339,6 +339,12 @@ declare class VideoSDKCore {
339
339
  getMeetingSummary(roomCode: string, sessionId: string): Promise<{
340
340
  summary: MeetingSummary | null;
341
341
  }>;
342
+ private acquireOptionalLocalMedia;
343
+ /**
344
+ * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
345
+ * whenever possible (Join, first click inside the meeting, etc.).
346
+ */
347
+ resumeActiveSpeaker(): Promise<boolean>;
342
348
  joinMeeting(config: Omit<MeetingConfig, "participantId">): Promise<void>;
343
349
  private connect;
344
350
  private createSFUClient;
@@ -427,6 +433,7 @@ type MeetingContextValue = {
427
433
  }>;
428
434
  toggleMic: () => void;
429
435
  toggleCam: () => void;
436
+ resumeActiveSpeaker: () => Promise<boolean>;
430
437
  startScreenShare: () => Promise<MediaStream | null>;
431
438
  stopScreenShare: () => void;
432
439
  sendMessage: (input: ChatInput) => void;
@@ -524,6 +531,7 @@ declare const useMeeting: (handlers?: {
524
531
  }>;
525
532
  toggleMic: () => void;
526
533
  toggleCam: () => void;
534
+ resumeActiveSpeaker: () => Promise<boolean>;
527
535
  startScreenShare: () => Promise<MediaStream | null>;
528
536
  stopScreenShare: () => void;
529
537
  sendMessage: (input: ChatInput) => void;
@@ -630,6 +638,15 @@ interface ActiveSpeakerDetectorOptions {
630
638
  releaseMs?: number;
631
639
  onActiveSpeakersChanged?: (participantIds: string[]) => void;
632
640
  }
641
+ /**
642
+ * Detects currently speaking participants from the actual audio tracks that
643
+ * are being received/published by WebRTC.
644
+ *
645
+ * The AudioContext is explicitly resumable so an application can wake it from
646
+ * a real user gesture (for example, the Join button). This matters in browsers
647
+ * that create Web Audio contexts in the "suspended" state until the page has
648
+ * received user interaction.
649
+ */
633
650
  declare class ActiveSpeakerDetector {
634
651
  private audioContext;
635
652
  private speakers;
@@ -642,7 +659,12 @@ declare class ActiveSpeakerDetector {
642
659
  private lastEmitted;
643
660
  constructor(options?: ActiveSpeakerDetectorOptions);
644
661
  private ensureAudioContext;
645
- resume(): Promise<void>;
662
+ /**
663
+ * Create/wake the Web Audio context. Call this from a user gesture when
664
+ * possible. Returns true when the context is running after the call.
665
+ */
666
+ resume(): Promise<boolean>;
667
+ getAudioContextState(): AudioContextState | "unavailable";
646
668
  addTrack(participantId: string, track: MediaStreamTrack): void;
647
669
  removeParticipant(participantId: string): void;
648
670
  removeTrack(track: MediaStreamTrack): void;
@@ -650,6 +672,7 @@ declare class ActiveSpeakerDetector {
650
672
  start(): void;
651
673
  stop(): void;
652
674
  private sample;
675
+ private getActiveSpeakers;
653
676
  private getActiveSpeakerIds;
654
677
  private emitIfChanged;
655
678
  destroy(): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -339,6 +339,12 @@ declare class VideoSDKCore {
339
339
  getMeetingSummary(roomCode: string, sessionId: string): Promise<{
340
340
  summary: MeetingSummary | null;
341
341
  }>;
342
+ private acquireOptionalLocalMedia;
343
+ /**
344
+ * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
345
+ * whenever possible (Join, first click inside the meeting, etc.).
346
+ */
347
+ resumeActiveSpeaker(): Promise<boolean>;
342
348
  joinMeeting(config: Omit<MeetingConfig, "participantId">): Promise<void>;
343
349
  private connect;
344
350
  private createSFUClient;
@@ -427,6 +433,7 @@ type MeetingContextValue = {
427
433
  }>;
428
434
  toggleMic: () => void;
429
435
  toggleCam: () => void;
436
+ resumeActiveSpeaker: () => Promise<boolean>;
430
437
  startScreenShare: () => Promise<MediaStream | null>;
431
438
  stopScreenShare: () => void;
432
439
  sendMessage: (input: ChatInput) => void;
@@ -524,6 +531,7 @@ declare const useMeeting: (handlers?: {
524
531
  }>;
525
532
  toggleMic: () => void;
526
533
  toggleCam: () => void;
534
+ resumeActiveSpeaker: () => Promise<boolean>;
527
535
  startScreenShare: () => Promise<MediaStream | null>;
528
536
  stopScreenShare: () => void;
529
537
  sendMessage: (input: ChatInput) => void;
@@ -630,6 +638,15 @@ interface ActiveSpeakerDetectorOptions {
630
638
  releaseMs?: number;
631
639
  onActiveSpeakersChanged?: (participantIds: string[]) => void;
632
640
  }
641
+ /**
642
+ * Detects currently speaking participants from the actual audio tracks that
643
+ * are being received/published by WebRTC.
644
+ *
645
+ * The AudioContext is explicitly resumable so an application can wake it from
646
+ * a real user gesture (for example, the Join button). This matters in browsers
647
+ * that create Web Audio contexts in the "suspended" state until the page has
648
+ * received user interaction.
649
+ */
633
650
  declare class ActiveSpeakerDetector {
634
651
  private audioContext;
635
652
  private speakers;
@@ -642,7 +659,12 @@ declare class ActiveSpeakerDetector {
642
659
  private lastEmitted;
643
660
  constructor(options?: ActiveSpeakerDetectorOptions);
644
661
  private ensureAudioContext;
645
- resume(): Promise<void>;
662
+ /**
663
+ * Create/wake the Web Audio context. Call this from a user gesture when
664
+ * possible. Returns true when the context is running after the call.
665
+ */
666
+ resume(): Promise<boolean>;
667
+ getAudioContextState(): AudioContextState | "unavailable";
646
668
  addTrack(participantId: string, track: MediaStreamTrack): void;
647
669
  removeParticipant(participantId: string): void;
648
670
  removeTrack(track: MediaStreamTrack): void;
@@ -650,6 +672,7 @@ declare class ActiveSpeakerDetector {
650
672
  start(): void;
651
673
  stop(): void;
652
674
  private sample;
675
+ private getActiveSpeakers;
653
676
  private getActiveSpeakerIds;
654
677
  private emitIfChanged;
655
678
  destroy(): Promise<void>;
package/dist/index.js CHANGED
@@ -63,13 +63,16 @@ var ActiveSpeakerDetector = class {
63
63
  this.timer = null;
64
64
  this.lastEmitted = "";
65
65
  this.intervalMs = options.intervalMs ?? 100;
66
- this.threshold = options.threshold ?? 0.025;
67
- this.releaseThreshold = options.releaseThreshold ?? 0.015;
68
- this.releaseMs = options.releaseMs ?? 600;
66
+ this.threshold = options.threshold ?? 0.02;
67
+ this.releaseThreshold = options.releaseThreshold ?? 0.01;
68
+ this.releaseMs = options.releaseMs ?? 650;
69
69
  this.onActiveSpeakersChanged = options.onActiveSpeakersChanged ?? (() => {
70
70
  });
71
71
  }
72
72
  ensureAudioContext() {
73
+ if (typeof window === "undefined") {
74
+ throw new Error("AudioContext is only available in the browser");
75
+ }
73
76
  if (!this.audioContext) {
74
77
  const AudioContextClass = window.AudioContext || window.webkitAudioContext;
75
78
  if (!AudioContextClass) {
@@ -79,28 +82,52 @@ var ActiveSpeakerDetector = class {
79
82
  }
80
83
  return this.audioContext;
81
84
  }
85
+ /**
86
+ * Create/wake the Web Audio context. Call this from a user gesture when
87
+ * possible. Returns true when the context is running after the call.
88
+ */
82
89
  async resume() {
83
- if (!this.audioContext) return;
84
- if (this.audioContext.state === "suspended") {
85
- try {
86
- await this.audioContext.resume();
87
- } catch {
90
+ try {
91
+ const context = this.ensureAudioContext();
92
+ if (context.state === "suspended") {
93
+ try {
94
+ await context.resume();
95
+ } catch (error) {
96
+ console.warn("[ActiveSpeaker] AudioContext resume failed", error);
97
+ }
88
98
  }
99
+ return context.state === "running";
100
+ } catch (error) {
101
+ console.warn("[ActiveSpeaker] AudioContext unavailable", error);
102
+ return false;
89
103
  }
90
104
  }
105
+ getAudioContextState() {
106
+ if (!this.audioContext) return "unavailable";
107
+ return this.audioContext.state;
108
+ }
91
109
  addTrack(participantId, track) {
92
- if (!participantId) return;
93
- if (track.kind !== "audio") return;
110
+ if (!participantId || track.kind !== "audio") return;
94
111
  this.removeParticipant(participantId);
95
- const context = this.ensureAudioContext();
112
+ let context;
113
+ try {
114
+ context = this.ensureAudioContext();
115
+ } catch (error) {
116
+ console.warn("[ActiveSpeaker] Could not create AudioContext", error);
117
+ return;
118
+ }
96
119
  const stream = new MediaStream([track]);
97
120
  const source = context.createMediaStreamSource(stream);
98
121
  const analyser = context.createAnalyser();
99
122
  analyser.fftSize = 512;
100
- analyser.smoothingTimeConstant = 0.65;
123
+ analyser.smoothingTimeConstant = 0.35;
101
124
  analyser.minDecibels = -90;
102
125
  analyser.maxDecibels = -10;
103
126
  source.connect(analyser);
127
+ const sink = context.createGain();
128
+ sink.gain.value = 0;
129
+ analyser.connect(sink);
130
+ sink.connect(context.destination);
104
131
  const data = new Float32Array(analyser.fftSize);
105
132
  const state = {
106
133
  participantId,
@@ -108,15 +135,20 @@ var ActiveSpeakerDetector = class {
108
135
  stream,
109
136
  source,
110
137
  analyser,
138
+ sink,
111
139
  data,
112
140
  active: false,
141
+ level: 0,
113
142
  lastActiveAt: 0
114
143
  };
115
144
  this.speakers.set(participantId, state);
116
145
  track.addEventListener(
117
146
  "ended",
118
147
  () => {
119
- this.removeParticipant(participantId);
148
+ const current = this.speakers.get(participantId);
149
+ if (current?.track === track) {
150
+ this.removeParticipant(participantId);
151
+ }
120
152
  },
121
153
  { once: true }
122
154
  );
@@ -127,15 +159,19 @@ var ActiveSpeakerDetector = class {
127
159
  const state = this.speakers.get(participantId);
128
160
  if (!state) return;
129
161
  state.active = false;
162
+ state.level = 0;
130
163
  try {
131
164
  state.source.disconnect();
132
165
  } catch {
133
166
  }
134
- state.stream.getTracks().forEach((track) => {
135
- if (track !== state.track) {
136
- track.stop();
137
- }
138
- });
167
+ try {
168
+ state.analyser.disconnect();
169
+ } catch {
170
+ }
171
+ try {
172
+ state.sink.disconnect();
173
+ } catch {
174
+ }
139
175
  this.speakers.delete(participantId);
140
176
  this.emitIfChanged();
141
177
  }
@@ -147,7 +183,7 @@ var ActiveSpeakerDetector = class {
147
183
  }
148
184
  }
149
185
  clear() {
150
- for (const participantId of this.speakers.keys()) {
186
+ for (const participantId of [...this.speakers.keys()]) {
151
187
  this.removeParticipant(participantId);
152
188
  }
153
189
  this.speakers.clear();
@@ -167,6 +203,7 @@ var ActiveSpeakerDetector = class {
167
203
  const now = performance.now();
168
204
  for (const state of this.speakers.values()) {
169
205
  if (state.track.readyState !== "live" || state.track.muted || !state.track.enabled) {
206
+ state.level = 0;
170
207
  if (state.active) {
171
208
  state.active = false;
172
209
  }
@@ -179,30 +216,33 @@ var ActiveSpeakerDetector = class {
179
216
  sumSquares += sample * sample;
180
217
  }
181
218
  const rms = Math.sqrt(sumSquares / state.data.length);
219
+ state.level = rms;
182
220
  if (!state.active) {
183
221
  if (rms >= this.threshold) {
184
222
  state.active = true;
185
223
  state.lastActiveAt = now;
186
224
  }
187
- } else {
188
- if (rms >= this.releaseThreshold) {
189
- state.lastActiveAt = now;
190
- } else if (now - state.lastActiveAt >= this.releaseMs) {
191
- state.active = false;
192
- }
225
+ } else if (rms >= this.releaseThreshold) {
226
+ state.lastActiveAt = now;
227
+ } else if (now - state.lastActiveAt >= this.releaseMs) {
228
+ state.active = false;
193
229
  }
194
230
  }
195
231
  this.emitIfChanged();
196
232
  }
233
+ getActiveSpeakers() {
234
+ return Array.from(this.speakers.values()).filter((speaker) => speaker.active).sort((a, b) => {
235
+ if (b.level !== a.level) return b.level - a.level;
236
+ return a.participantId.localeCompare(b.participantId);
237
+ });
238
+ }
197
239
  getActiveSpeakerIds() {
198
- return Array.from(this.speakers.values()).filter((speaker) => speaker.active).map((speaker) => speaker.participantId);
240
+ return this.getActiveSpeakers().map((speaker) => speaker.participantId);
199
241
  }
200
242
  emitIfChanged(force = false) {
201
- const ids = this.getActiveSpeakerIds().sort();
243
+ const ids = this.getActiveSpeakerIds();
202
244
  const key = ids.join("|");
203
- if (!force && key === this.lastEmitted) {
204
- return;
205
- }
245
+ if (!force && key === this.lastEmitted) return;
206
246
  this.lastEmitted = key;
207
247
  this.onActiveSpeakersChanged(ids);
208
248
  }
@@ -2347,26 +2387,11 @@ var VideoSDKCore = class {
2347
2387
  async initLocal(video, name) {
2348
2388
  this.participantName = name;
2349
2389
  try {
2350
- this.localStream = await navigator.mediaDevices.getUserMedia({
2351
- video: {
2352
- width: { ideal: 1280 },
2353
- height: { ideal: 720 }
2354
- },
2355
- audio: {
2356
- echoCancellation: true,
2357
- noiseSuppression: true,
2358
- autoGainControl: true
2359
- }
2360
- });
2390
+ this.localStream = await this.acquireOptionalLocalMedia();
2361
2391
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2362
2392
  if (localAudioTrack) {
2363
2393
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
2364
2394
  }
2365
- const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
2366
- const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
2367
- if (!hasVideo || !hasAudio) {
2368
- throw new Error(`Missing tracks: video=${hasVideo}, audio=${hasAudio}`);
2369
- }
2370
2395
  video.srcObject = this.localStream;
2371
2396
  this.updateLocalMedia();
2372
2397
  } catch (err) {
@@ -2383,8 +2408,8 @@ var VideoSDKCore = class {
2383
2408
  stream: this.localStream,
2384
2409
  cameraTrack: this.localStream.getVideoTracks()[0],
2385
2410
  audioTrack: this.localStream.getAudioTracks()[0],
2386
- micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? !audioMuted,
2387
- camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? !videoMuted,
2411
+ micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? false,
2412
+ camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? false,
2388
2413
  isScreenSharing: this.isScreenSharing,
2389
2414
  screenStream: this.screenStream,
2390
2415
  screenTrack: this.screenStream?.getVideoTracks()[0]
@@ -2411,6 +2436,70 @@ var VideoSDKCore = class {
2411
2436
  async getMeetingSummary(roomCode, sessionId) {
2412
2437
  return this.sfuClient.getMeetingSummary(roomCode, sessionId);
2413
2438
  }
2439
+ async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId) {
2440
+ const combined = new MediaStream();
2441
+ if (!navigator.mediaDevices?.getUserMedia) {
2442
+ console.warn(
2443
+ "[SDK] Camera/microphone access is not supported; joining without local media."
2444
+ );
2445
+ return combined;
2446
+ }
2447
+ const cameraPromise = navigator.mediaDevices.getUserMedia({
2448
+ video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
2449
+ width: { ideal: 1280 },
2450
+ height: { ideal: 720 }
2451
+ },
2452
+ audio: false
2453
+ });
2454
+ const microphonePromise = navigator.mediaDevices.getUserMedia({
2455
+ video: false,
2456
+ audio: microphoneDeviceId ? {
2457
+ deviceId: { exact: microphoneDeviceId },
2458
+ echoCancellation: true,
2459
+ noiseSuppression: true,
2460
+ autoGainControl: true
2461
+ } : {
2462
+ echoCancellation: true,
2463
+ noiseSuppression: true,
2464
+ autoGainControl: true
2465
+ }
2466
+ });
2467
+ const [cameraResult, microphoneResult] = await Promise.allSettled([
2468
+ cameraPromise,
2469
+ microphonePromise
2470
+ ]);
2471
+ if (cameraResult.status === "fulfilled") {
2472
+ for (const track of cameraResult.value.getVideoTracks()) {
2473
+ combined.addTrack(track);
2474
+ }
2475
+ } else {
2476
+ console.warn("[SDK] Camera unavailable; continuing without video.", {
2477
+ name: cameraResult.reason?.name,
2478
+ message: cameraResult.reason?.message
2479
+ });
2480
+ }
2481
+ if (microphoneResult.status === "fulfilled") {
2482
+ for (const track of microphoneResult.value.getAudioTracks()) {
2483
+ combined.addTrack(track);
2484
+ }
2485
+ } else {
2486
+ console.warn(
2487
+ "[SDK] Microphone unavailable; continuing without audio.",
2488
+ {
2489
+ name: microphoneResult.reason?.name,
2490
+ message: microphoneResult.reason?.message
2491
+ }
2492
+ );
2493
+ }
2494
+ return combined;
2495
+ }
2496
+ /**
2497
+ * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
2498
+ * whenever possible (Join, first click inside the meeting, etc.).
2499
+ */
2500
+ async resumeActiveSpeaker() {
2501
+ return this.activeSpeakerDetector.resume();
2502
+ }
2414
2503
  // JOIN
2415
2504
  async joinMeeting(config) {
2416
2505
  const {
@@ -2454,15 +2543,10 @@ var VideoSDKCore = class {
2454
2543
  this.room.id = roomId;
2455
2544
  this.room.name = name;
2456
2545
  if (!this.localStream) {
2457
- this.localStream = await navigator.mediaDevices.getUserMedia({
2458
- video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : true,
2459
- audio: microphoneDeviceId ? {
2460
- deviceId: { exact: microphoneDeviceId },
2461
- echoCancellation: true,
2462
- noiseSuppression: true,
2463
- autoGainControl: true
2464
- } : true
2465
- });
2546
+ this.localStream = await this.acquireOptionalLocalMedia(
2547
+ cameraDeviceId,
2548
+ microphoneDeviceId
2549
+ );
2466
2550
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2467
2551
  if (localAudioTrack) {
2468
2552
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
@@ -2507,8 +2591,8 @@ var VideoSDKCore = class {
2507
2591
  await this.sfuClient.getToken(roomId, {
2508
2592
  displayName: name,
2509
2593
  participantId: this.myId,
2510
- audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? true),
2511
- videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? true)
2594
+ audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? false),
2595
+ videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? false)
2512
2596
  });
2513
2597
  }
2514
2598
  createSFUClient(token) {
@@ -3568,6 +3652,7 @@ var MeetingProvider = ({
3568
3652
  getMeetingSummary: sdk.getMeetingSummary.bind(sdk),
3569
3653
  toggleMic: sdk.toggleMic.bind(sdk),
3570
3654
  toggleCam: sdk.toggleCam.bind(sdk),
3655
+ resumeActiveSpeaker: sdk.resumeActiveSpeaker.bind(sdk),
3571
3656
  startScreenShare: sdk.startScreenShare.bind(sdk),
3572
3657
  stopScreenShare: sdk.stopScreenShare.bind(sdk),
3573
3658
  sendMessage: sdk.sendChatMessage.bind(sdk),