@defcomm/meetingsdk 0.6.7 → 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
@@ -11,6 +11,7 @@ type Events = {
11
11
  onUserJoined?: (p: Participant) => void;
12
12
  /** Fired when a remote participant permanently joins the meeting. */
13
13
  onParticipantJoined?: (p: Participant) => void;
14
+ onParticipantPresence?: (participantId: string, presence: string) => void;
14
15
  onEntryRequested?: (req: EntryRequest) => void;
15
16
  onEntryResponded?: (payload: {
16
17
  participantId: string;
@@ -285,6 +286,12 @@ declare class MeetingState {
285
286
  clearRecording(): void;
286
287
  setNetworkQuality(quality: NetworkQuality | null): void;
287
288
  addParticipant(p: Participant): boolean;
289
+ /**
290
+ * Reconcile a participant received from a room snapshot without emitting a
291
+ * lifecycle/join event. Snapshot data is authoritative for current room
292
+ * membership, including after a reconnect/resume.
293
+ */
294
+ upsertParticipantSnapshot(p: Participant): void;
288
295
  removeParticipant(id: string): void;
289
296
  updateParticipantMedia(id: string, patch: Partial<NonNullable<Participant["media"]>>): void;
290
297
  updateLocalParticipant(patch: LocalParticipantPatch): void;
@@ -332,6 +339,12 @@ declare class VideoSDKCore {
332
339
  getMeetingSummary(roomCode: string, sessionId: string): Promise<{
333
340
  summary: MeetingSummary | null;
334
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>;
335
348
  joinMeeting(config: Omit<MeetingConfig, "participantId">): Promise<void>;
336
349
  private connect;
337
350
  private createSFUClient;
@@ -420,6 +433,7 @@ type MeetingContextValue = {
420
433
  }>;
421
434
  toggleMic: () => void;
422
435
  toggleCam: () => void;
436
+ resumeActiveSpeaker: () => Promise<boolean>;
423
437
  startScreenShare: () => Promise<MediaStream | null>;
424
438
  stopScreenShare: () => void;
425
439
  sendMessage: (input: ChatInput) => void;
@@ -468,6 +482,7 @@ type MeetingContextValue = {
468
482
  onNetworkQuality: (cb: (quality: NetworkQuality) => void) => () => void;
469
483
  onChatMessage: (cb: (message: ChatMessage) => void) => () => void;
470
484
  onParticipantJoined: (cb: (participant: Participant) => void) => () => void;
485
+ onParticipantPresence: (cb: (participantId: string, presence: string) => void) => () => void;
471
486
  onParticipantLeft: (cb: (participant: Participant) => void) => () => void;
472
487
  };
473
488
  declare const MeetingProvider: ({ config, children, }: {
@@ -516,6 +531,7 @@ declare const useMeeting: (handlers?: {
516
531
  }>;
517
532
  toggleMic: () => void;
518
533
  toggleCam: () => void;
534
+ resumeActiveSpeaker: () => Promise<boolean>;
519
535
  startScreenShare: () => Promise<MediaStream | null>;
520
536
  stopScreenShare: () => void;
521
537
  sendMessage: (input: ChatInput) => void;
@@ -564,6 +580,7 @@ declare const useMeeting: (handlers?: {
564
580
  onNetworkQuality: (cb: (quality: NetworkQuality) => void) => () => void;
565
581
  onChatMessage: (cb: (message: ChatMessage) => void) => () => void;
566
582
  onParticipantJoined: (cb: (participant: Participant) => void) => () => void;
583
+ onParticipantPresence: (cb: (participantId: string, presence: string) => void) => () => void;
567
584
  onParticipantLeft: (cb: (participant: Participant) => void) => () => void;
568
585
  };
569
586
 
@@ -621,6 +638,15 @@ interface ActiveSpeakerDetectorOptions {
621
638
  releaseMs?: number;
622
639
  onActiveSpeakersChanged?: (participantIds: string[]) => void;
623
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
+ */
624
650
  declare class ActiveSpeakerDetector {
625
651
  private audioContext;
626
652
  private speakers;
@@ -633,7 +659,12 @@ declare class ActiveSpeakerDetector {
633
659
  private lastEmitted;
634
660
  constructor(options?: ActiveSpeakerDetectorOptions);
635
661
  private ensureAudioContext;
636
- 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";
637
668
  addTrack(participantId: string, track: MediaStreamTrack): void;
638
669
  removeParticipant(participantId: string): void;
639
670
  removeTrack(track: MediaStreamTrack): void;
@@ -641,6 +672,7 @@ declare class ActiveSpeakerDetector {
641
672
  start(): void;
642
673
  stop(): void;
643
674
  private sample;
675
+ private getActiveSpeakers;
644
676
  private getActiveSpeakerIds;
645
677
  private emitIfChanged;
646
678
  destroy(): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ type Events = {
11
11
  onUserJoined?: (p: Participant) => void;
12
12
  /** Fired when a remote participant permanently joins the meeting. */
13
13
  onParticipantJoined?: (p: Participant) => void;
14
+ onParticipantPresence?: (participantId: string, presence: string) => void;
14
15
  onEntryRequested?: (req: EntryRequest) => void;
15
16
  onEntryResponded?: (payload: {
16
17
  participantId: string;
@@ -285,6 +286,12 @@ declare class MeetingState {
285
286
  clearRecording(): void;
286
287
  setNetworkQuality(quality: NetworkQuality | null): void;
287
288
  addParticipant(p: Participant): boolean;
289
+ /**
290
+ * Reconcile a participant received from a room snapshot without emitting a
291
+ * lifecycle/join event. Snapshot data is authoritative for current room
292
+ * membership, including after a reconnect/resume.
293
+ */
294
+ upsertParticipantSnapshot(p: Participant): void;
288
295
  removeParticipant(id: string): void;
289
296
  updateParticipantMedia(id: string, patch: Partial<NonNullable<Participant["media"]>>): void;
290
297
  updateLocalParticipant(patch: LocalParticipantPatch): void;
@@ -332,6 +339,12 @@ declare class VideoSDKCore {
332
339
  getMeetingSummary(roomCode: string, sessionId: string): Promise<{
333
340
  summary: MeetingSummary | null;
334
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>;
335
348
  joinMeeting(config: Omit<MeetingConfig, "participantId">): Promise<void>;
336
349
  private connect;
337
350
  private createSFUClient;
@@ -420,6 +433,7 @@ type MeetingContextValue = {
420
433
  }>;
421
434
  toggleMic: () => void;
422
435
  toggleCam: () => void;
436
+ resumeActiveSpeaker: () => Promise<boolean>;
423
437
  startScreenShare: () => Promise<MediaStream | null>;
424
438
  stopScreenShare: () => void;
425
439
  sendMessage: (input: ChatInput) => void;
@@ -468,6 +482,7 @@ type MeetingContextValue = {
468
482
  onNetworkQuality: (cb: (quality: NetworkQuality) => void) => () => void;
469
483
  onChatMessage: (cb: (message: ChatMessage) => void) => () => void;
470
484
  onParticipantJoined: (cb: (participant: Participant) => void) => () => void;
485
+ onParticipantPresence: (cb: (participantId: string, presence: string) => void) => () => void;
471
486
  onParticipantLeft: (cb: (participant: Participant) => void) => () => void;
472
487
  };
473
488
  declare const MeetingProvider: ({ config, children, }: {
@@ -516,6 +531,7 @@ declare const useMeeting: (handlers?: {
516
531
  }>;
517
532
  toggleMic: () => void;
518
533
  toggleCam: () => void;
534
+ resumeActiveSpeaker: () => Promise<boolean>;
519
535
  startScreenShare: () => Promise<MediaStream | null>;
520
536
  stopScreenShare: () => void;
521
537
  sendMessage: (input: ChatInput) => void;
@@ -564,6 +580,7 @@ declare const useMeeting: (handlers?: {
564
580
  onNetworkQuality: (cb: (quality: NetworkQuality) => void) => () => void;
565
581
  onChatMessage: (cb: (message: ChatMessage) => void) => () => void;
566
582
  onParticipantJoined: (cb: (participant: Participant) => void) => () => void;
583
+ onParticipantPresence: (cb: (participantId: string, presence: string) => void) => () => void;
567
584
  onParticipantLeft: (cb: (participant: Participant) => void) => () => void;
568
585
  };
569
586
 
@@ -621,6 +638,15 @@ interface ActiveSpeakerDetectorOptions {
621
638
  releaseMs?: number;
622
639
  onActiveSpeakersChanged?: (participantIds: string[]) => void;
623
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
+ */
624
650
  declare class ActiveSpeakerDetector {
625
651
  private audioContext;
626
652
  private speakers;
@@ -633,7 +659,12 @@ declare class ActiveSpeakerDetector {
633
659
  private lastEmitted;
634
660
  constructor(options?: ActiveSpeakerDetectorOptions);
635
661
  private ensureAudioContext;
636
- 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";
637
668
  addTrack(participantId: string, track: MediaStreamTrack): void;
638
669
  removeParticipant(participantId: string): void;
639
670
  removeTrack(track: MediaStreamTrack): void;
@@ -641,6 +672,7 @@ declare class ActiveSpeakerDetector {
641
672
  start(): void;
642
673
  stop(): void;
643
674
  private sample;
675
+ private getActiveSpeakers;
644
676
  private getActiveSpeakerIds;
645
677
  private emitIfChanged;
646
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
  }
@@ -279,6 +319,36 @@ var MeetingState = class {
279
319
  this.notify("participants");
280
320
  return true;
281
321
  }
322
+ /**
323
+ * Reconcile a participant received from a room snapshot without emitting a
324
+ * lifecycle/join event. Snapshot data is authoritative for current room
325
+ * membership, including after a reconnect/resume.
326
+ */
327
+ upsertParticipantSnapshot(p) {
328
+ if (!p?.id) return;
329
+ const current = this.participants.get(p.id);
330
+ const updated = {
331
+ ...current ?? {},
332
+ ...p,
333
+ media: {
334
+ stream: current?.media?.stream ?? null,
335
+ screenStream: current?.media?.screenStream,
336
+ cameraTrack: current?.media?.cameraTrack,
337
+ screenTrack: current?.media?.screenTrack,
338
+ audioTrack: current?.media?.audioTrack,
339
+ micEnabled: current?.media?.micEnabled ?? !(p.media?.micEnabled === false),
340
+ camEnabled: current?.media?.camEnabled ?? !(p.media?.camEnabled === false),
341
+ isScreenSharing: current?.media?.isScreenSharing ?? false,
342
+ ...current?.media,
343
+ ...p.media
344
+ }
345
+ };
346
+ const next = new Map(this.participants);
347
+ next.set(p.id, updated);
348
+ this.participants = next;
349
+ this.notify(`participant:${p.id}`);
350
+ this.notify("participants");
351
+ }
282
352
  removeParticipant(id) {
283
353
  const next = new Map(this.participants);
284
354
  next.delete(id);
@@ -1243,7 +1313,6 @@ var SFUClient = class {
1243
1313
  }
1244
1314
  for (const participant of this.participants.values()) {
1245
1315
  if (participant.participant_id !== this.currentParticipantId) {
1246
- this.onParticipantJoined(participant);
1247
1316
  this.onParticipantPresence(
1248
1317
  participant.participant_id,
1249
1318
  String(participant.presence || "connected")
@@ -2318,26 +2387,11 @@ var VideoSDKCore = class {
2318
2387
  async initLocal(video, name) {
2319
2388
  this.participantName = name;
2320
2389
  try {
2321
- this.localStream = await navigator.mediaDevices.getUserMedia({
2322
- video: {
2323
- width: { ideal: 1280 },
2324
- height: { ideal: 720 }
2325
- },
2326
- audio: {
2327
- echoCancellation: true,
2328
- noiseSuppression: true,
2329
- autoGainControl: true
2330
- }
2331
- });
2390
+ this.localStream = await this.acquireOptionalLocalMedia();
2332
2391
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2333
2392
  if (localAudioTrack) {
2334
2393
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
2335
2394
  }
2336
- const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
2337
- const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
2338
- if (!hasVideo || !hasAudio) {
2339
- throw new Error(`Missing tracks: video=${hasVideo}, audio=${hasAudio}`);
2340
- }
2341
2395
  video.srcObject = this.localStream;
2342
2396
  this.updateLocalMedia();
2343
2397
  } catch (err) {
@@ -2354,8 +2408,8 @@ var VideoSDKCore = class {
2354
2408
  stream: this.localStream,
2355
2409
  cameraTrack: this.localStream.getVideoTracks()[0],
2356
2410
  audioTrack: this.localStream.getAudioTracks()[0],
2357
- micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? !audioMuted,
2358
- camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? !videoMuted,
2411
+ micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? false,
2412
+ camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? false,
2359
2413
  isScreenSharing: this.isScreenSharing,
2360
2414
  screenStream: this.screenStream,
2361
2415
  screenTrack: this.screenStream?.getVideoTracks()[0]
@@ -2382,6 +2436,70 @@ var VideoSDKCore = class {
2382
2436
  async getMeetingSummary(roomCode, sessionId) {
2383
2437
  return this.sfuClient.getMeetingSummary(roomCode, sessionId);
2384
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
+ }
2385
2503
  // JOIN
2386
2504
  async joinMeeting(config) {
2387
2505
  const {
@@ -2425,15 +2543,10 @@ var VideoSDKCore = class {
2425
2543
  this.room.id = roomId;
2426
2544
  this.room.name = name;
2427
2545
  if (!this.localStream) {
2428
- this.localStream = await navigator.mediaDevices.getUserMedia({
2429
- video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : true,
2430
- audio: microphoneDeviceId ? {
2431
- deviceId: { exact: microphoneDeviceId },
2432
- echoCancellation: true,
2433
- noiseSuppression: true,
2434
- autoGainControl: true
2435
- } : true
2436
- });
2546
+ this.localStream = await this.acquireOptionalLocalMedia(
2547
+ cameraDeviceId,
2548
+ microphoneDeviceId
2549
+ );
2437
2550
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2438
2551
  if (localAudioTrack) {
2439
2552
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
@@ -2478,8 +2591,8 @@ var VideoSDKCore = class {
2478
2591
  await this.sfuClient.getToken(roomId, {
2479
2592
  displayName: name,
2480
2593
  participantId: this.myId,
2481
- audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? true),
2482
- videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? true)
2594
+ audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? false),
2595
+ videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? false)
2483
2596
  });
2484
2597
  }
2485
2598
  createSFUClient(token) {
@@ -2528,6 +2641,36 @@ var VideoSDKCore = class {
2528
2641
  },
2529
2642
  onRoomUpdate: (payload) => {
2530
2643
  const participants = payload?.participants || [];
2644
+ const snapshotParticipantIds = /* @__PURE__ */ new Set();
2645
+ for (const participant of participants) {
2646
+ const pid = participant?.participant_id;
2647
+ if (!pid || pid === this.myId) continue;
2648
+ snapshotParticipantIds.add(pid);
2649
+ const current = this.state.getParticipant(pid);
2650
+ const name = participant?.user_metadata?.name || participant?.display_name || current?.name || "Participant";
2651
+ this.state.upsertParticipantSnapshot({
2652
+ id: pid,
2653
+ name,
2654
+ isModerator: Boolean(participant?.permissions?.moderate),
2655
+ isHost: Boolean(
2656
+ participant?.is_host ?? participant?.isHost ?? participant?.permissions?.host
2657
+ ),
2658
+ handRaised: Boolean(participant?.hand_raised),
2659
+ permissions: participant?.permissions,
2660
+ connectionState: participant?.presence === "reconnecting" ? "reconnecting" : participant?.presence === "disconnected" ? "disconnected" : "connected",
2661
+ media: {
2662
+ ...current?.media || {},
2663
+ micEnabled: !Boolean(participant?.audio_muted),
2664
+ camEnabled: !Boolean(participant?.video_muted),
2665
+ isScreenSharing: Boolean(participant?.screen_sharing)
2666
+ }
2667
+ });
2668
+ }
2669
+ for (const [pid] of this.state.participants) {
2670
+ if (pid === this.myId || snapshotParticipantIds.has(pid)) continue;
2671
+ this.cleanupRemoteParticipant(pid);
2672
+ this.state.removeParticipant(pid);
2673
+ }
2531
2674
  const local = participants.find(
2532
2675
  (p) => p?.participant_id === this.myId
2533
2676
  );
@@ -3341,6 +3484,9 @@ var MeetingProvider = ({
3341
3484
  const participantJoinedListeners = (0, import_react2.useRef)(
3342
3485
  /* @__PURE__ */ new Set()
3343
3486
  );
3487
+ const participantPresenceListeners = (0, import_react2.useRef)(
3488
+ /* @__PURE__ */ new Set()
3489
+ );
3344
3490
  const participantLeftListeners = (0, import_react2.useRef)(
3345
3491
  /* @__PURE__ */ new Set()
3346
3492
  );
@@ -3429,7 +3575,9 @@ var MeetingProvider = ({
3429
3575
  pubSubListeners.current.get("REACTION")?.forEach((fn) => fn(message));
3430
3576
  },
3431
3577
  onHandRaised: (participantId, raised) => {
3432
- handRaisedListeners.current.forEach((fn) => fn(participantId, raised));
3578
+ handRaisedListeners.current.forEach(
3579
+ (fn) => fn(participantId, raised)
3580
+ );
3433
3581
  },
3434
3582
  onNetworkQuality: (quality) => {
3435
3583
  networkQualityListeners.current.forEach((fn) => fn(quality));
@@ -3442,6 +3590,11 @@ var MeetingProvider = ({
3442
3590
  onParticipantJoined: (participant) => {
3443
3591
  participantJoinedListeners.current.forEach((fn) => fn(participant));
3444
3592
  },
3593
+ onParticipantPresence: (participantId, presence) => {
3594
+ participantPresenceListeners.current.forEach(
3595
+ (fn) => fn(participantId, presence)
3596
+ );
3597
+ },
3445
3598
  onParticipantLeft: (participant) => {
3446
3599
  participantLeftListeners.current.forEach((fn) => fn(participant));
3447
3600
  }
@@ -3499,6 +3652,7 @@ var MeetingProvider = ({
3499
3652
  getMeetingSummary: sdk.getMeetingSummary.bind(sdk),
3500
3653
  toggleMic: sdk.toggleMic.bind(sdk),
3501
3654
  toggleCam: sdk.toggleCam.bind(sdk),
3655
+ resumeActiveSpeaker: sdk.resumeActiveSpeaker.bind(sdk),
3502
3656
  startScreenShare: sdk.startScreenShare.bind(sdk),
3503
3657
  stopScreenShare: sdk.stopScreenShare.bind(sdk),
3504
3658
  sendMessage: sdk.sendChatMessage.bind(sdk),
@@ -3654,6 +3808,10 @@ var MeetingProvider = ({
3654
3808
  participantJoinedListeners.current.add(cb);
3655
3809
  return () => participantJoinedListeners.current.delete(cb);
3656
3810
  },
3811
+ onParticipantPresence: (cb) => {
3812
+ participantPresenceListeners.current.add(cb);
3813
+ return () => participantPresenceListeners.current.delete(cb);
3814
+ },
3657
3815
  onChatMessage: (cb) => {
3658
3816
  chatMessageListeners.current.add(cb);
3659
3817
  return () => chatMessageListeners.current.delete(cb);