@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.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
  }
@@ -377,7 +417,7 @@ var MeetingState = class {
377
417
  const nextMedia = {
378
418
  stream: patch.media?.stream ?? prevMedia.stream,
379
419
  screenStream: patch.media?.screenStream ?? prevMedia.screenStream,
380
- cameraTrack: patch.media?.cameraTrack ?? prevMedia.cameraTrack,
420
+ cameraTrack: patch.media && Object.prototype.hasOwnProperty.call(patch.media, "cameraTrack") ? patch.media.cameraTrack ?? void 0 : prevMedia.cameraTrack,
381
421
  screenTrack: patch.media?.screenTrack ?? prevMedia.screenTrack,
382
422
  audioTrack: patch.media?.audioTrack ?? prevMedia.audioTrack,
383
423
  micEnabled: patch.media?.micEnabled ?? prevMedia.micEnabled,
@@ -638,6 +678,71 @@ var SFUClient = class {
638
678
  setLocalStream(stream) {
639
679
  this.localStream = stream;
640
680
  }
681
+ /**
682
+ * Attach or detach the local camera track without replacing the publisher
683
+ * PeerConnection. Detaching with replaceTrack(null) stops media transmission;
684
+ * VideoSDKCore separately calls MediaStreamTrack.stop() to release hardware.
685
+ */
686
+ async setCameraTrack(track) {
687
+ if (!this.localStream) this.localStream = new MediaStream();
688
+ const currentTrack = this.localStream.getVideoTracks().find((item) => item !== track);
689
+ if (currentTrack) this.localStream.removeTrack(currentTrack);
690
+ if (track && !this.localStream.getTracks().includes(track)) {
691
+ this.localStream.addTrack(track);
692
+ }
693
+ if (!this.videoSender || !this.publisher || !this.cameraTransceiver) {
694
+ return;
695
+ }
696
+ const needsCameraNegotiation = Boolean(
697
+ track && this.cameraTransceiver.direction === "inactive"
698
+ );
699
+ if (needsCameraNegotiation && this.ws?.readyState !== WebSocket.OPEN) {
700
+ throw new Error("Cannot enable the camera while signaling is disconnected");
701
+ }
702
+ await this.videoSender.replaceTrack(track);
703
+ if (needsCameraNegotiation) {
704
+ await this.waitForPublisherStable();
705
+ this.cameraTransceiver.direction = "sendonly";
706
+ await this.sendPublisherOffer();
707
+ }
708
+ }
709
+ async waitForPublisherStable(timeoutMs = 5e3) {
710
+ const publisher = this.publisher;
711
+ if (!publisher) throw new Error("Publisher PeerConnection is not initialized");
712
+ const startedAt = Date.now();
713
+ while (publisher.signalingState !== "stable") {
714
+ if (publisher.signalingState === "closed") {
715
+ throw new Error("Publisher PeerConnection is closed");
716
+ }
717
+ if (Date.now() - startedAt >= timeoutMs) {
718
+ throw new Error("Publisher negotiation did not become stable in time");
719
+ }
720
+ await new Promise((resolve) => setTimeout(resolve, 25));
721
+ }
722
+ }
723
+ async sendPublisherOffer() {
724
+ if (!this.publisher) throw new Error("Publisher PeerConnection is not initialized");
725
+ const offer = await this.publisher.createOffer();
726
+ await this.publisher.setLocalDescription(offer);
727
+ const sent = this.send({
728
+ type: "publish_offer",
729
+ payload: {
730
+ revision: ++this.publisherRevision,
731
+ sdp: offer.sdp
732
+ }
733
+ });
734
+ if (!sent) {
735
+ try {
736
+ await this.publisher.setLocalDescription({ type: "rollback" });
737
+ } catch {
738
+ }
739
+ if (this.cameraTransceiver) {
740
+ this.cameraTransceiver.direction = "inactive";
741
+ }
742
+ await this.videoSender?.replaceTrack(null);
743
+ throw new Error("Unable to negotiate camera publishing: signaling is disconnected");
744
+ }
745
+ }
641
746
  nextRequestId() {
642
747
  return (++this.requestId).toString();
643
748
  }
@@ -2315,6 +2420,8 @@ var VideoSDKCore = class {
2315
2420
  this.screenStream = null;
2316
2421
  this.isScreenSharing = false;
2317
2422
  this.participantName = "";
2423
+ this.cameraEnabled = false;
2424
+ this.cameraToggleInProgress = false;
2318
2425
  this.intentionalDisconnect = false;
2319
2426
  this.remoteStreams = /* @__PURE__ */ new Map();
2320
2427
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
@@ -2347,26 +2454,12 @@ var VideoSDKCore = class {
2347
2454
  async initLocal(video, name) {
2348
2455
  this.participantName = name;
2349
2456
  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
- });
2457
+ this.localStream = await this.acquireOptionalLocalMedia();
2458
+ this.cameraEnabled = this.hasLiveCameraTrack();
2361
2459
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2362
2460
  if (localAudioTrack) {
2363
2461
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
2364
2462
  }
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
2463
  video.srcObject = this.localStream;
2371
2464
  this.updateLocalMedia();
2372
2465
  } catch (err) {
@@ -2374,17 +2467,47 @@ var VideoSDKCore = class {
2374
2467
  throw err;
2375
2468
  }
2376
2469
  }
2377
- updateLocalMedia(audioMuted = false, videoMuted = false) {
2470
+ hasLiveCameraTrack() {
2471
+ return Boolean(
2472
+ this.localStream?.getVideoTracks().some((track) => track.readyState === "live")
2473
+ );
2474
+ }
2475
+ getLiveCameraTrack() {
2476
+ return this.localStream?.getVideoTracks().find((track) => track.readyState === "live") ?? null;
2477
+ }
2478
+ /**
2479
+ * Stop every locally captured camera track and remove it from the stream.
2480
+ * Track.enabled=false only emits black frames; stop() releases camera capture.
2481
+ */
2482
+ stopCameraCapture() {
2483
+ if (!this.localStream) {
2484
+ this.cameraEnabled = false;
2485
+ return;
2486
+ }
2487
+ for (const track of this.localStream.getVideoTracks()) {
2488
+ this.localStream.removeTrack(track);
2489
+ try {
2490
+ track.stop();
2491
+ } catch {
2492
+ }
2493
+ }
2494
+ this.cameraEnabled = false;
2495
+ }
2496
+ updateLocalMedia() {
2378
2497
  if (!this.localStream) return;
2498
+ const cameraTrack = this.getLiveCameraTrack();
2499
+ const audioTrack = this.localStream.getAudioTracks().find((track) => track.readyState === "live");
2500
+ this.cameraEnabled = Boolean(cameraTrack && cameraTrack.enabled);
2379
2501
  this.state.updateLocalParticipant({
2380
2502
  id: this.myId,
2381
2503
  name: this.participantName,
2382
2504
  media: {
2383
2505
  stream: this.localStream,
2384
- cameraTrack: this.localStream.getVideoTracks()[0],
2385
- audioTrack: this.localStream.getAudioTracks()[0],
2386
- micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? !audioMuted,
2387
- camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? !videoMuted,
2506
+ // Explicit null clears a previously published track from React state.
2507
+ cameraTrack,
2508
+ audioTrack: audioTrack ?? void 0,
2509
+ micEnabled: audioTrack?.enabled ?? false,
2510
+ camEnabled: this.cameraEnabled,
2388
2511
  isScreenSharing: this.isScreenSharing,
2389
2512
  screenStream: this.screenStream,
2390
2513
  screenTrack: this.screenStream?.getVideoTracks()[0]
@@ -2411,6 +2534,89 @@ var VideoSDKCore = class {
2411
2534
  async getMeetingSummary(roomCode, sessionId) {
2412
2535
  return this.sfuClient.getMeetingSummary(roomCode, sessionId);
2413
2536
  }
2537
+ async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId, includeVideo = true) {
2538
+ const combined = new MediaStream();
2539
+ if (!navigator.mediaDevices?.getUserMedia) {
2540
+ console.warn(
2541
+ "[SDK] Camera/microphone access is not supported; joining without local media."
2542
+ );
2543
+ return combined;
2544
+ }
2545
+ const cameraPromise = includeVideo ? navigator.mediaDevices.getUserMedia({
2546
+ video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
2547
+ width: { ideal: 1280 },
2548
+ height: { ideal: 720 }
2549
+ },
2550
+ audio: false
2551
+ }) : Promise.resolve(null);
2552
+ const microphonePromise = navigator.mediaDevices.getUserMedia({
2553
+ video: false,
2554
+ audio: microphoneDeviceId ? {
2555
+ deviceId: { exact: microphoneDeviceId },
2556
+ echoCancellation: true,
2557
+ noiseSuppression: true,
2558
+ autoGainControl: true
2559
+ } : {
2560
+ echoCancellation: true,
2561
+ noiseSuppression: true,
2562
+ autoGainControl: true
2563
+ }
2564
+ });
2565
+ const [cameraResult, microphoneResult] = await Promise.allSettled([
2566
+ cameraPromise,
2567
+ microphonePromise
2568
+ ]);
2569
+ if (cameraResult.status === "fulfilled" && cameraResult.value) {
2570
+ for (const track of cameraResult.value.getVideoTracks()) {
2571
+ combined.addTrack(track);
2572
+ }
2573
+ } else if (cameraResult.status === "rejected") {
2574
+ console.warn("[SDK] Camera unavailable; continuing without video.", {
2575
+ name: cameraResult.reason?.name,
2576
+ message: cameraResult.reason?.message
2577
+ });
2578
+ }
2579
+ if (microphoneResult.status === "fulfilled") {
2580
+ for (const track of microphoneResult.value.getAudioTracks()) {
2581
+ combined.addTrack(track);
2582
+ }
2583
+ } else {
2584
+ console.warn(
2585
+ "[SDK] Microphone unavailable; continuing without audio.",
2586
+ {
2587
+ name: microphoneResult.reason?.name,
2588
+ message: microphoneResult.reason?.message
2589
+ }
2590
+ );
2591
+ }
2592
+ return combined;
2593
+ }
2594
+ async acquireCameraTrack() {
2595
+ if (!navigator.mediaDevices?.getUserMedia) {
2596
+ throw new Error("Camera access is not supported on this device");
2597
+ }
2598
+ const stream = await navigator.mediaDevices.getUserMedia({
2599
+ video: this.cameraDeviceId ? { deviceId: { exact: this.cameraDeviceId } } : {
2600
+ width: { ideal: 1280 },
2601
+ height: { ideal: 720 }
2602
+ },
2603
+ audio: false
2604
+ });
2605
+ const track = stream.getVideoTracks()[0];
2606
+ if (!track) {
2607
+ stream.getTracks().forEach((item) => item.stop());
2608
+ throw new Error("The selected camera did not provide a video track");
2609
+ }
2610
+ this.cameraDeviceId = track.getSettings().deviceId || this.cameraDeviceId;
2611
+ return track;
2612
+ }
2613
+ /**
2614
+ * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
2615
+ * whenever possible (Join, first click inside the meeting, etc.).
2616
+ */
2617
+ async resumeActiveSpeaker() {
2618
+ return this.activeSpeakerDetector.resume();
2619
+ }
2414
2620
  // JOIN
2415
2621
  async joinMeeting(config) {
2416
2622
  const {
@@ -2453,16 +2659,13 @@ var VideoSDKCore = class {
2453
2659
  this.participantName = name;
2454
2660
  this.room.id = roomId;
2455
2661
  this.room.name = name;
2662
+ this.cameraDeviceId = cameraDeviceId ?? this.cameraDeviceId;
2456
2663
  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
- });
2664
+ this.localStream = await this.acquireOptionalLocalMedia(
2665
+ this.cameraDeviceId,
2666
+ microphoneDeviceId,
2667
+ !videoMuted
2668
+ );
2466
2669
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2467
2670
  if (localAudioTrack) {
2468
2671
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
@@ -2471,10 +2674,17 @@ var VideoSDKCore = class {
2471
2674
  this.localStream.getAudioTracks().forEach((track) => {
2472
2675
  track.enabled = !audioMuted;
2473
2676
  });
2474
- this.localStream.getVideoTracks().forEach((track) => {
2475
- track.enabled = !videoMuted;
2476
- });
2477
- this.updateLocalMedia(audioMuted, videoMuted);
2677
+ if (videoMuted) {
2678
+ this.stopCameraCapture();
2679
+ } else {
2680
+ const initialCameraTrack = this.getLiveCameraTrack();
2681
+ this.cameraEnabled = Boolean(initialCameraTrack);
2682
+ if (initialCameraTrack) {
2683
+ this.cameraDeviceId = initialCameraTrack.getSettings().deviceId || this.cameraDeviceId;
2684
+ initialCameraTrack.enabled = true;
2685
+ }
2686
+ }
2687
+ this.updateLocalMedia();
2478
2688
  const promise = this.connect(roomId, name);
2479
2689
  this.joinPromise = promise;
2480
2690
  try {
@@ -2507,8 +2717,8 @@ var VideoSDKCore = class {
2507
2717
  await this.sfuClient.getToken(roomId, {
2508
2718
  displayName: name,
2509
2719
  participantId: this.myId,
2510
- audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? true),
2511
- videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? true)
2720
+ audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? false),
2721
+ videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? false)
2512
2722
  });
2513
2723
  }
2514
2724
  createSFUClient(token) {
@@ -3101,7 +3311,12 @@ var VideoSDKCore = class {
3101
3311
  if (!this.sfuClient) {
3102
3312
  throw new Error("SFU client is not initialized");
3103
3313
  }
3314
+ if (!this.cameraEnabled) {
3315
+ throw new Error("Your camera is off. Turn it on before switching devices.");
3316
+ }
3104
3317
  const track = await this.sfuClient.selectCamera(deviceId);
3318
+ this.cameraDeviceId = deviceId;
3319
+ this.cameraEnabled = true;
3105
3320
  if (this.localStream) {
3106
3321
  this.state.localStream = this.localStream;
3107
3322
  this.updateLocalMedia();
@@ -3143,24 +3358,52 @@ var VideoSDKCore = class {
3143
3358
  this.sfuClient?.muteAudio(!nextEnabled);
3144
3359
  this.events.onMicToggled?.(this.myId, nextEnabled);
3145
3360
  }
3146
- toggleCam() {
3147
- const mediaState = this.state.localParticipant?.media;
3148
- if (!mediaState || !this.localStream) {
3149
- return;
3150
- }
3151
- const nextEnabled = !mediaState.camEnabled;
3152
- this.localStream.getVideoTracks().forEach((track) => {
3153
- track.enabled = nextEnabled;
3154
- });
3155
- this.state.updateLocalParticipant({
3156
- id: this.myId,
3157
- name: this.participantName,
3158
- media: {
3159
- camEnabled: nextEnabled
3361
+ async toggleCam() {
3362
+ if (!this.state.localParticipant?.media || !this.localStream) return;
3363
+ if (this.cameraToggleInProgress) return;
3364
+ this.cameraToggleInProgress = true;
3365
+ const nextEnabled = !this.cameraEnabled;
3366
+ try {
3367
+ if (!nextEnabled) {
3368
+ this.sfuClient?.muteVideo(true);
3369
+ this.stopCameraCapture();
3370
+ await this.sfuClient?.setCameraTrack(null);
3371
+ this.updateLocalMedia();
3372
+ this.events.onCamToggled?.(this.myId, false);
3373
+ return;
3160
3374
  }
3161
- });
3162
- this.sfuClient?.muteVideo(!nextEnabled);
3163
- this.events.onCamToggled?.(this.myId, nextEnabled);
3375
+ const track = await this.acquireCameraTrack();
3376
+ if (!this.localStream) this.localStream = new MediaStream();
3377
+ this.localStream.addTrack(track);
3378
+ try {
3379
+ await this.sfuClient?.setCameraTrack(track);
3380
+ this.cameraEnabled = true;
3381
+ this.sfuClient?.muteVideo(false);
3382
+ this.updateLocalMedia();
3383
+ this.events.onCamToggled?.(this.myId, true);
3384
+ } catch (error) {
3385
+ this.localStream.removeTrack(track);
3386
+ try {
3387
+ track.stop();
3388
+ } catch {
3389
+ }
3390
+ this.cameraEnabled = false;
3391
+ this.updateLocalMedia();
3392
+ throw error;
3393
+ }
3394
+ } catch (error) {
3395
+ this.cameraEnabled = false;
3396
+ this.updateLocalMedia();
3397
+ this.emitError(
3398
+ "CAMERA_TOGGLE_FAILED",
3399
+ error?.message || "Unable to change camera state",
3400
+ error,
3401
+ true
3402
+ );
3403
+ throw error;
3404
+ } finally {
3405
+ this.cameraToggleInProgress = false;
3406
+ }
3164
3407
  }
3165
3408
  // SCREEN SHARING
3166
3409
  async startScreenShare() {
@@ -3318,6 +3561,8 @@ var VideoSDKCore = class {
3318
3561
  this.localStream.getTracks().forEach((track) => track.stop());
3319
3562
  this.localStream = null;
3320
3563
  }
3564
+ this.cameraEnabled = false;
3565
+ this.cameraToggleInProgress = false;
3321
3566
  if (this.screenStream) {
3322
3567
  this.screenStream.getTracks().forEach((track) => track.stop());
3323
3568
  this.screenStream = null;
@@ -3568,6 +3813,7 @@ var MeetingProvider = ({
3568
3813
  getMeetingSummary: sdk.getMeetingSummary.bind(sdk),
3569
3814
  toggleMic: sdk.toggleMic.bind(sdk),
3570
3815
  toggleCam: sdk.toggleCam.bind(sdk),
3816
+ resumeActiveSpeaker: sdk.resumeActiveSpeaker.bind(sdk),
3571
3817
  startScreenShare: sdk.startScreenShare.bind(sdk),
3572
3818
  stopScreenShare: sdk.stopScreenShare.bind(sdk),
3573
3819
  sendMessage: sdk.sendChatMessage.bind(sdk),