@defcomm/meetingsdk 0.6.8 → 0.7.1

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
  }
@@ -346,7 +386,7 @@ var MeetingState = class {
346
386
  const nextMedia = {
347
387
  stream: patch.media?.stream ?? prevMedia.stream,
348
388
  screenStream: patch.media?.screenStream ?? prevMedia.screenStream,
349
- cameraTrack: patch.media?.cameraTrack ?? prevMedia.cameraTrack,
389
+ cameraTrack: patch.media && Object.prototype.hasOwnProperty.call(patch.media, "cameraTrack") ? patch.media.cameraTrack ?? void 0 : prevMedia.cameraTrack,
350
390
  screenTrack: patch.media?.screenTrack ?? prevMedia.screenTrack,
351
391
  audioTrack: patch.media?.audioTrack ?? prevMedia.audioTrack,
352
392
  micEnabled: patch.media?.micEnabled ?? prevMedia.micEnabled,
@@ -607,6 +647,71 @@ var SFUClient = class {
607
647
  setLocalStream(stream) {
608
648
  this.localStream = stream;
609
649
  }
650
+ /**
651
+ * Attach or detach the local camera track without replacing the publisher
652
+ * PeerConnection. Detaching with replaceTrack(null) stops media transmission;
653
+ * VideoSDKCore separately calls MediaStreamTrack.stop() to release hardware.
654
+ */
655
+ async setCameraTrack(track) {
656
+ if (!this.localStream) this.localStream = new MediaStream();
657
+ const currentTrack = this.localStream.getVideoTracks().find((item) => item !== track);
658
+ if (currentTrack) this.localStream.removeTrack(currentTrack);
659
+ if (track && !this.localStream.getTracks().includes(track)) {
660
+ this.localStream.addTrack(track);
661
+ }
662
+ if (!this.videoSender || !this.publisher || !this.cameraTransceiver) {
663
+ return;
664
+ }
665
+ const needsCameraNegotiation = Boolean(
666
+ track && this.cameraTransceiver.direction === "inactive"
667
+ );
668
+ if (needsCameraNegotiation && this.ws?.readyState !== WebSocket.OPEN) {
669
+ throw new Error("Cannot enable the camera while signaling is disconnected");
670
+ }
671
+ await this.videoSender.replaceTrack(track);
672
+ if (needsCameraNegotiation) {
673
+ await this.waitForPublisherStable();
674
+ this.cameraTransceiver.direction = "sendonly";
675
+ await this.sendPublisherOffer();
676
+ }
677
+ }
678
+ async waitForPublisherStable(timeoutMs = 5e3) {
679
+ const publisher = this.publisher;
680
+ if (!publisher) throw new Error("Publisher PeerConnection is not initialized");
681
+ const startedAt = Date.now();
682
+ while (publisher.signalingState !== "stable") {
683
+ if (publisher.signalingState === "closed") {
684
+ throw new Error("Publisher PeerConnection is closed");
685
+ }
686
+ if (Date.now() - startedAt >= timeoutMs) {
687
+ throw new Error("Publisher negotiation did not become stable in time");
688
+ }
689
+ await new Promise((resolve) => setTimeout(resolve, 25));
690
+ }
691
+ }
692
+ async sendPublisherOffer() {
693
+ if (!this.publisher) throw new Error("Publisher PeerConnection is not initialized");
694
+ const offer = await this.publisher.createOffer();
695
+ await this.publisher.setLocalDescription(offer);
696
+ const sent = this.send({
697
+ type: "publish_offer",
698
+ payload: {
699
+ revision: ++this.publisherRevision,
700
+ sdp: offer.sdp
701
+ }
702
+ });
703
+ if (!sent) {
704
+ try {
705
+ await this.publisher.setLocalDescription({ type: "rollback" });
706
+ } catch {
707
+ }
708
+ if (this.cameraTransceiver) {
709
+ this.cameraTransceiver.direction = "inactive";
710
+ }
711
+ await this.videoSender?.replaceTrack(null);
712
+ throw new Error("Unable to negotiate camera publishing: signaling is disconnected");
713
+ }
714
+ }
610
715
  nextRequestId() {
611
716
  return (++this.requestId).toString();
612
717
  }
@@ -2284,6 +2389,8 @@ var VideoSDKCore = class {
2284
2389
  this.screenStream = null;
2285
2390
  this.isScreenSharing = false;
2286
2391
  this.participantName = "";
2392
+ this.cameraEnabled = false;
2393
+ this.cameraToggleInProgress = false;
2287
2394
  this.intentionalDisconnect = false;
2288
2395
  this.remoteStreams = /* @__PURE__ */ new Map();
2289
2396
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
@@ -2316,26 +2423,12 @@ var VideoSDKCore = class {
2316
2423
  async initLocal(video, name) {
2317
2424
  this.participantName = name;
2318
2425
  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
- });
2426
+ this.localStream = await this.acquireOptionalLocalMedia();
2427
+ this.cameraEnabled = this.hasLiveCameraTrack();
2330
2428
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2331
2429
  if (localAudioTrack) {
2332
2430
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
2333
2431
  }
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
2432
  video.srcObject = this.localStream;
2340
2433
  this.updateLocalMedia();
2341
2434
  } catch (err) {
@@ -2343,17 +2436,47 @@ var VideoSDKCore = class {
2343
2436
  throw err;
2344
2437
  }
2345
2438
  }
2346
- updateLocalMedia(audioMuted = false, videoMuted = false) {
2439
+ hasLiveCameraTrack() {
2440
+ return Boolean(
2441
+ this.localStream?.getVideoTracks().some((track) => track.readyState === "live")
2442
+ );
2443
+ }
2444
+ getLiveCameraTrack() {
2445
+ return this.localStream?.getVideoTracks().find((track) => track.readyState === "live") ?? null;
2446
+ }
2447
+ /**
2448
+ * Stop every locally captured camera track and remove it from the stream.
2449
+ * Track.enabled=false only emits black frames; stop() releases camera capture.
2450
+ */
2451
+ stopCameraCapture() {
2452
+ if (!this.localStream) {
2453
+ this.cameraEnabled = false;
2454
+ return;
2455
+ }
2456
+ for (const track of this.localStream.getVideoTracks()) {
2457
+ this.localStream.removeTrack(track);
2458
+ try {
2459
+ track.stop();
2460
+ } catch {
2461
+ }
2462
+ }
2463
+ this.cameraEnabled = false;
2464
+ }
2465
+ updateLocalMedia() {
2347
2466
  if (!this.localStream) return;
2467
+ const cameraTrack = this.getLiveCameraTrack();
2468
+ const audioTrack = this.localStream.getAudioTracks().find((track) => track.readyState === "live");
2469
+ this.cameraEnabled = Boolean(cameraTrack && cameraTrack.enabled);
2348
2470
  this.state.updateLocalParticipant({
2349
2471
  id: this.myId,
2350
2472
  name: this.participantName,
2351
2473
  media: {
2352
2474
  stream: this.localStream,
2353
- cameraTrack: this.localStream.getVideoTracks()[0],
2354
- audioTrack: this.localStream.getAudioTracks()[0],
2355
- micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? !audioMuted,
2356
- camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? !videoMuted,
2475
+ // Explicit null clears a previously published track from React state.
2476
+ cameraTrack,
2477
+ audioTrack: audioTrack ?? void 0,
2478
+ micEnabled: audioTrack?.enabled ?? false,
2479
+ camEnabled: this.cameraEnabled,
2357
2480
  isScreenSharing: this.isScreenSharing,
2358
2481
  screenStream: this.screenStream,
2359
2482
  screenTrack: this.screenStream?.getVideoTracks()[0]
@@ -2380,6 +2503,89 @@ var VideoSDKCore = class {
2380
2503
  async getMeetingSummary(roomCode, sessionId) {
2381
2504
  return this.sfuClient.getMeetingSummary(roomCode, sessionId);
2382
2505
  }
2506
+ async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId, includeVideo = true) {
2507
+ const combined = new MediaStream();
2508
+ if (!navigator.mediaDevices?.getUserMedia) {
2509
+ console.warn(
2510
+ "[SDK] Camera/microphone access is not supported; joining without local media."
2511
+ );
2512
+ return combined;
2513
+ }
2514
+ const cameraPromise = includeVideo ? navigator.mediaDevices.getUserMedia({
2515
+ video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
2516
+ width: { ideal: 1280 },
2517
+ height: { ideal: 720 }
2518
+ },
2519
+ audio: false
2520
+ }) : Promise.resolve(null);
2521
+ const microphonePromise = navigator.mediaDevices.getUserMedia({
2522
+ video: false,
2523
+ audio: microphoneDeviceId ? {
2524
+ deviceId: { exact: microphoneDeviceId },
2525
+ echoCancellation: true,
2526
+ noiseSuppression: true,
2527
+ autoGainControl: true
2528
+ } : {
2529
+ echoCancellation: true,
2530
+ noiseSuppression: true,
2531
+ autoGainControl: true
2532
+ }
2533
+ });
2534
+ const [cameraResult, microphoneResult] = await Promise.allSettled([
2535
+ cameraPromise,
2536
+ microphonePromise
2537
+ ]);
2538
+ if (cameraResult.status === "fulfilled" && cameraResult.value) {
2539
+ for (const track of cameraResult.value.getVideoTracks()) {
2540
+ combined.addTrack(track);
2541
+ }
2542
+ } else if (cameraResult.status === "rejected") {
2543
+ console.warn("[SDK] Camera unavailable; continuing without video.", {
2544
+ name: cameraResult.reason?.name,
2545
+ message: cameraResult.reason?.message
2546
+ });
2547
+ }
2548
+ if (microphoneResult.status === "fulfilled") {
2549
+ for (const track of microphoneResult.value.getAudioTracks()) {
2550
+ combined.addTrack(track);
2551
+ }
2552
+ } else {
2553
+ console.warn(
2554
+ "[SDK] Microphone unavailable; continuing without audio.",
2555
+ {
2556
+ name: microphoneResult.reason?.name,
2557
+ message: microphoneResult.reason?.message
2558
+ }
2559
+ );
2560
+ }
2561
+ return combined;
2562
+ }
2563
+ async acquireCameraTrack() {
2564
+ if (!navigator.mediaDevices?.getUserMedia) {
2565
+ throw new Error("Camera access is not supported on this device");
2566
+ }
2567
+ const stream = await navigator.mediaDevices.getUserMedia({
2568
+ video: this.cameraDeviceId ? { deviceId: { exact: this.cameraDeviceId } } : {
2569
+ width: { ideal: 1280 },
2570
+ height: { ideal: 720 }
2571
+ },
2572
+ audio: false
2573
+ });
2574
+ const track = stream.getVideoTracks()[0];
2575
+ if (!track) {
2576
+ stream.getTracks().forEach((item) => item.stop());
2577
+ throw new Error("The selected camera did not provide a video track");
2578
+ }
2579
+ this.cameraDeviceId = track.getSettings().deviceId || this.cameraDeviceId;
2580
+ return track;
2581
+ }
2582
+ /**
2583
+ * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
2584
+ * whenever possible (Join, first click inside the meeting, etc.).
2585
+ */
2586
+ async resumeActiveSpeaker() {
2587
+ return this.activeSpeakerDetector.resume();
2588
+ }
2383
2589
  // JOIN
2384
2590
  async joinMeeting(config) {
2385
2591
  const {
@@ -2422,16 +2628,13 @@ var VideoSDKCore = class {
2422
2628
  this.participantName = name;
2423
2629
  this.room.id = roomId;
2424
2630
  this.room.name = name;
2631
+ this.cameraDeviceId = cameraDeviceId ?? this.cameraDeviceId;
2425
2632
  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
- });
2633
+ this.localStream = await this.acquireOptionalLocalMedia(
2634
+ this.cameraDeviceId,
2635
+ microphoneDeviceId,
2636
+ !videoMuted
2637
+ );
2435
2638
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2436
2639
  if (localAudioTrack) {
2437
2640
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
@@ -2440,10 +2643,17 @@ var VideoSDKCore = class {
2440
2643
  this.localStream.getAudioTracks().forEach((track) => {
2441
2644
  track.enabled = !audioMuted;
2442
2645
  });
2443
- this.localStream.getVideoTracks().forEach((track) => {
2444
- track.enabled = !videoMuted;
2445
- });
2446
- this.updateLocalMedia(audioMuted, videoMuted);
2646
+ if (videoMuted) {
2647
+ this.stopCameraCapture();
2648
+ } else {
2649
+ const initialCameraTrack = this.getLiveCameraTrack();
2650
+ this.cameraEnabled = Boolean(initialCameraTrack);
2651
+ if (initialCameraTrack) {
2652
+ this.cameraDeviceId = initialCameraTrack.getSettings().deviceId || this.cameraDeviceId;
2653
+ initialCameraTrack.enabled = true;
2654
+ }
2655
+ }
2656
+ this.updateLocalMedia();
2447
2657
  const promise = this.connect(roomId, name);
2448
2658
  this.joinPromise = promise;
2449
2659
  try {
@@ -2476,8 +2686,8 @@ var VideoSDKCore = class {
2476
2686
  await this.sfuClient.getToken(roomId, {
2477
2687
  displayName: name,
2478
2688
  participantId: this.myId,
2479
- audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? true),
2480
- videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? true)
2689
+ audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? false),
2690
+ videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? false)
2481
2691
  });
2482
2692
  }
2483
2693
  createSFUClient(token) {
@@ -3070,7 +3280,12 @@ var VideoSDKCore = class {
3070
3280
  if (!this.sfuClient) {
3071
3281
  throw new Error("SFU client is not initialized");
3072
3282
  }
3283
+ if (!this.cameraEnabled) {
3284
+ throw new Error("Your camera is off. Turn it on before switching devices.");
3285
+ }
3073
3286
  const track = await this.sfuClient.selectCamera(deviceId);
3287
+ this.cameraDeviceId = deviceId;
3288
+ this.cameraEnabled = true;
3074
3289
  if (this.localStream) {
3075
3290
  this.state.localStream = this.localStream;
3076
3291
  this.updateLocalMedia();
@@ -3112,24 +3327,52 @@ var VideoSDKCore = class {
3112
3327
  this.sfuClient?.muteAudio(!nextEnabled);
3113
3328
  this.events.onMicToggled?.(this.myId, nextEnabled);
3114
3329
  }
3115
- toggleCam() {
3116
- const mediaState = this.state.localParticipant?.media;
3117
- if (!mediaState || !this.localStream) {
3118
- return;
3119
- }
3120
- const nextEnabled = !mediaState.camEnabled;
3121
- this.localStream.getVideoTracks().forEach((track) => {
3122
- track.enabled = nextEnabled;
3123
- });
3124
- this.state.updateLocalParticipant({
3125
- id: this.myId,
3126
- name: this.participantName,
3127
- media: {
3128
- camEnabled: nextEnabled
3330
+ async toggleCam() {
3331
+ if (!this.state.localParticipant?.media || !this.localStream) return;
3332
+ if (this.cameraToggleInProgress) return;
3333
+ this.cameraToggleInProgress = true;
3334
+ const nextEnabled = !this.cameraEnabled;
3335
+ try {
3336
+ if (!nextEnabled) {
3337
+ this.sfuClient?.muteVideo(true);
3338
+ this.stopCameraCapture();
3339
+ await this.sfuClient?.setCameraTrack(null);
3340
+ this.updateLocalMedia();
3341
+ this.events.onCamToggled?.(this.myId, false);
3342
+ return;
3129
3343
  }
3130
- });
3131
- this.sfuClient?.muteVideo(!nextEnabled);
3132
- this.events.onCamToggled?.(this.myId, nextEnabled);
3344
+ const track = await this.acquireCameraTrack();
3345
+ if (!this.localStream) this.localStream = new MediaStream();
3346
+ this.localStream.addTrack(track);
3347
+ try {
3348
+ await this.sfuClient?.setCameraTrack(track);
3349
+ this.cameraEnabled = true;
3350
+ this.sfuClient?.muteVideo(false);
3351
+ this.updateLocalMedia();
3352
+ this.events.onCamToggled?.(this.myId, true);
3353
+ } catch (error) {
3354
+ this.localStream.removeTrack(track);
3355
+ try {
3356
+ track.stop();
3357
+ } catch {
3358
+ }
3359
+ this.cameraEnabled = false;
3360
+ this.updateLocalMedia();
3361
+ throw error;
3362
+ }
3363
+ } catch (error) {
3364
+ this.cameraEnabled = false;
3365
+ this.updateLocalMedia();
3366
+ this.emitError(
3367
+ "CAMERA_TOGGLE_FAILED",
3368
+ error?.message || "Unable to change camera state",
3369
+ error,
3370
+ true
3371
+ );
3372
+ throw error;
3373
+ } finally {
3374
+ this.cameraToggleInProgress = false;
3375
+ }
3133
3376
  }
3134
3377
  // SCREEN SHARING
3135
3378
  async startScreenShare() {
@@ -3287,6 +3530,8 @@ var VideoSDKCore = class {
3287
3530
  this.localStream.getTracks().forEach((track) => track.stop());
3288
3531
  this.localStream = null;
3289
3532
  }
3533
+ this.cameraEnabled = false;
3534
+ this.cameraToggleInProgress = false;
3290
3535
  if (this.screenStream) {
3291
3536
  this.screenStream.getTracks().forEach((track) => track.stop());
3292
3537
  this.screenStream = null;
@@ -3537,6 +3782,7 @@ var MeetingProvider = ({
3537
3782
  getMeetingSummary: sdk.getMeetingSummary.bind(sdk),
3538
3783
  toggleMic: sdk.toggleMic.bind(sdk),
3539
3784
  toggleCam: sdk.toggleCam.bind(sdk),
3785
+ resumeActiveSpeaker: sdk.resumeActiveSpeaker.bind(sdk),
3540
3786
  startScreenShare: sdk.startScreenShare.bind(sdk),
3541
3787
  stopScreenShare: sdk.stopScreenShare.bind(sdk),
3542
3788
  sendMessage: sdk.sendChatMessage.bind(sdk),