@defcomm/meetingsdk 0.6.9 → 0.7.2

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/README.md CHANGED
@@ -92,6 +92,7 @@ Your backend
92
92
  Do **not** put a server-only API secret in browser code. Your backend should handle server credentials and issue the short-lived/access-scoped token that the browser uses for the meeting.
93
93
 
94
94
  Your application is responsible for deciding who the participant is and for obtaining the token securely.
95
+ The SDK uses the supplied `config.token` for REST and SFU authentication. It does not request or refresh tokens; reconnects reuse the same token.
95
96
 
96
97
  ---
97
98
 
@@ -421,10 +422,26 @@ Toggle the local camera:
421
422
  ```tsx
422
423
  const { toggleCam } = useMeeting();
423
424
 
424
- <button onClick={toggleCam}>Toggle camera</button>;
425
+ <button
426
+ onClick={() =>
427
+ void toggleCam().catch((error) => {
428
+ console.error("Unable to toggle camera", error);
429
+ })
430
+ }
431
+ >
432
+ Toggle camera
433
+ </button>;
425
434
  ```
426
435
 
427
- The local participant state exposes `media.micEnabled` and `media.camEnabled`, so the button can reflect the current state.
436
+ Camera-off is a privacy action: the SDK detaches and stops the active camera
437
+ `MediaStreamTrack`, releasing the browser's camera capture. It does not merely
438
+ set `track.enabled = false`. Turning the camera back on explicitly requests a
439
+ new camera track and may trigger the browser's permission prompt if access was
440
+ revoked. Joining with `videoMuted: true` does not request camera access at all.
441
+
442
+ The local participant state exposes `media.micEnabled` and `media.camEnabled`,
443
+ so the button can reflect the current state. `toggleCam()` returns a Promise;
444
+ handle errors if camera permission is denied or the device is unavailable.
428
445
 
429
446
  ---
430
447
 
@@ -811,7 +828,7 @@ export function Meeting() {
811
828
  <div>
812
829
  <button onClick={() => void join()}>Join</button>
813
830
  <button onClick={() => toggleMic()}>Mic</button>
814
- <button onClick={() => toggleCam()}>Camera</button>
831
+ <button onClick={() => void toggleCam()}>Camera</button>
815
832
  <button onClick={() => void startScreenShare()}>
816
833
  Share screen
817
834
  </button>
package/dist/index.d.mts CHANGED
@@ -112,7 +112,7 @@ type Participant = {
112
112
  type ParticipantMedia = {
113
113
  stream?: MediaStream | null;
114
114
  screenStream?: MediaStream | null;
115
- cameraTrack?: MediaStreamTrack;
115
+ cameraTrack?: MediaStreamTrack | null;
116
116
  screenTrack?: MediaStreamTrack;
117
117
  audioTrack?: MediaStreamTrack;
118
118
  micEnabled: boolean;
@@ -315,6 +315,9 @@ declare class VideoSDKCore {
315
315
  private screenStream;
316
316
  private isScreenSharing;
317
317
  private participantName;
318
+ private cameraDeviceId?;
319
+ private cameraEnabled;
320
+ private cameraToggleInProgress;
318
321
  readonly state: MeetingState;
319
322
  private intentionalDisconnect;
320
323
  private remoteStreams;
@@ -325,6 +328,13 @@ declare class VideoSDKCore {
325
328
  private emitError;
326
329
  constructor(options: VideoSDKCoreOptions, events?: Events, apiBase?: string, wsBase?: string);
327
330
  initLocal(video: HTMLVideoElement, name: string): Promise<void>;
331
+ private hasLiveCameraTrack;
332
+ private getLiveCameraTrack;
333
+ /**
334
+ * Stop every locally captured camera track and remove it from the stream.
335
+ * Track.enabled=false only emits black frames; stop() releases camera capture.
336
+ */
337
+ private stopCameraCapture;
328
338
  private updateLocalMedia;
329
339
  createRoom(input: {
330
340
  name: string;
@@ -340,6 +350,7 @@ declare class VideoSDKCore {
340
350
  summary: MeetingSummary | null;
341
351
  }>;
342
352
  private acquireOptionalLocalMedia;
353
+ private acquireCameraTrack;
343
354
  /**
344
355
  * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
345
356
  * whenever possible (Join, first click inside the meeting, etc.).
@@ -367,7 +378,7 @@ declare class VideoSDKCore {
367
378
  selectCamera(deviceId: string): Promise<MediaStreamTrack>;
368
379
  selectMicrophone(deviceId: string): Promise<MediaStreamTrack>;
369
380
  toggleMic(): void;
370
- toggleCam(): void;
381
+ toggleCam(): Promise<void>;
371
382
  startScreenShare(): Promise<MediaStream | null>;
372
383
  stopScreenShare(): Promise<void>;
373
384
  startRecording(): Promise<void>;
@@ -432,7 +443,7 @@ type MeetingContextValue = {
432
443
  summary: MeetingSummary | null;
433
444
  }>;
434
445
  toggleMic: () => void;
435
- toggleCam: () => void;
446
+ toggleCam: () => Promise<void>;
436
447
  resumeActiveSpeaker: () => Promise<boolean>;
437
448
  startScreenShare: () => Promise<MediaStream | null>;
438
449
  stopScreenShare: () => void;
@@ -530,7 +541,7 @@ declare const useMeeting: (handlers?: {
530
541
  summary: MeetingSummary | null;
531
542
  }>;
532
543
  toggleMic: () => void;
533
- toggleCam: () => void;
544
+ toggleCam: () => Promise<void>;
534
545
  resumeActiveSpeaker: () => Promise<boolean>;
535
546
  startScreenShare: () => Promise<MediaStream | null>;
536
547
  stopScreenShare: () => void;
package/dist/index.d.ts CHANGED
@@ -112,7 +112,7 @@ type Participant = {
112
112
  type ParticipantMedia = {
113
113
  stream?: MediaStream | null;
114
114
  screenStream?: MediaStream | null;
115
- cameraTrack?: MediaStreamTrack;
115
+ cameraTrack?: MediaStreamTrack | null;
116
116
  screenTrack?: MediaStreamTrack;
117
117
  audioTrack?: MediaStreamTrack;
118
118
  micEnabled: boolean;
@@ -315,6 +315,9 @@ declare class VideoSDKCore {
315
315
  private screenStream;
316
316
  private isScreenSharing;
317
317
  private participantName;
318
+ private cameraDeviceId?;
319
+ private cameraEnabled;
320
+ private cameraToggleInProgress;
318
321
  readonly state: MeetingState;
319
322
  private intentionalDisconnect;
320
323
  private remoteStreams;
@@ -325,6 +328,13 @@ declare class VideoSDKCore {
325
328
  private emitError;
326
329
  constructor(options: VideoSDKCoreOptions, events?: Events, apiBase?: string, wsBase?: string);
327
330
  initLocal(video: HTMLVideoElement, name: string): Promise<void>;
331
+ private hasLiveCameraTrack;
332
+ private getLiveCameraTrack;
333
+ /**
334
+ * Stop every locally captured camera track and remove it from the stream.
335
+ * Track.enabled=false only emits black frames; stop() releases camera capture.
336
+ */
337
+ private stopCameraCapture;
328
338
  private updateLocalMedia;
329
339
  createRoom(input: {
330
340
  name: string;
@@ -340,6 +350,7 @@ declare class VideoSDKCore {
340
350
  summary: MeetingSummary | null;
341
351
  }>;
342
352
  private acquireOptionalLocalMedia;
353
+ private acquireCameraTrack;
343
354
  /**
344
355
  * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
345
356
  * whenever possible (Join, first click inside the meeting, etc.).
@@ -367,7 +378,7 @@ declare class VideoSDKCore {
367
378
  selectCamera(deviceId: string): Promise<MediaStreamTrack>;
368
379
  selectMicrophone(deviceId: string): Promise<MediaStreamTrack>;
369
380
  toggleMic(): void;
370
- toggleCam(): void;
381
+ toggleCam(): Promise<void>;
371
382
  startScreenShare(): Promise<MediaStream | null>;
372
383
  stopScreenShare(): Promise<void>;
373
384
  startRecording(): Promise<void>;
@@ -432,7 +443,7 @@ type MeetingContextValue = {
432
443
  summary: MeetingSummary | null;
433
444
  }>;
434
445
  toggleMic: () => void;
435
- toggleCam: () => void;
446
+ toggleCam: () => Promise<void>;
436
447
  resumeActiveSpeaker: () => Promise<boolean>;
437
448
  startScreenShare: () => Promise<MediaStream | null>;
438
449
  stopScreenShare: () => void;
@@ -530,7 +541,7 @@ declare const useMeeting: (handlers?: {
530
541
  summary: MeetingSummary | null;
531
542
  }>;
532
543
  toggleMic: () => void;
533
- toggleCam: () => void;
544
+ toggleCam: () => Promise<void>;
534
545
  resumeActiveSpeaker: () => Promise<boolean>;
535
546
  startScreenShare: () => Promise<MediaStream | null>;
536
547
  stopScreenShare: () => void;
package/dist/index.js CHANGED
@@ -417,7 +417,7 @@ var MeetingState = class {
417
417
  const nextMedia = {
418
418
  stream: patch.media?.stream ?? prevMedia.stream,
419
419
  screenStream: patch.media?.screenStream ?? prevMedia.screenStream,
420
- cameraTrack: patch.media?.cameraTrack ?? prevMedia.cameraTrack,
420
+ cameraTrack: patch.media && Object.prototype.hasOwnProperty.call(patch.media, "cameraTrack") ? patch.media.cameraTrack ?? void 0 : prevMedia.cameraTrack,
421
421
  screenTrack: patch.media?.screenTrack ?? prevMedia.screenTrack,
422
422
  audioTrack: patch.media?.audioTrack ?? prevMedia.audioTrack,
423
423
  micEnabled: patch.media?.micEnabled ?? prevMedia.micEnabled,
@@ -504,9 +504,9 @@ var SFUClient = class {
504
504
  this.publisherRevision = 0;
505
505
  this.screenShareGeneration = 0;
506
506
  this.currentParticipantId = null;
507
- // Provider/application token used for authenticated REST calls.
507
+ // Token supplied by the application for REST and SFU authentication.
508
508
  this.authToken = null;
509
- // Room/SFU token returned by /token and used for the WebSocket.
509
+ // Token used for the current WebSocket session.
510
510
  this.token = null;
511
511
  this.room = null;
512
512
  this.resumeToken = null;
@@ -678,6 +678,71 @@ var SFUClient = class {
678
678
  setLocalStream(stream) {
679
679
  this.localStream = stream;
680
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
+ }
681
746
  nextRequestId() {
682
747
  return (++this.requestId).toString();
683
748
  }
@@ -732,7 +797,8 @@ var SFUClient = class {
732
797
  }
733
798
  );
734
799
  }
735
- async getToken(roomCode, options = {}) {
800
+ async join(roomCode, options = {}) {
801
+ if (!this.authToken) throw new Error("Authentication token is required");
736
802
  const nextRoom = roomCode.trim();
737
803
  if (this.room && this.room !== nextRoom) {
738
804
  this.resumeToken = null;
@@ -750,33 +816,7 @@ var SFUClient = class {
750
816
  if (!this.resumeToken) {
751
817
  this.resumeToken = this.loadResumeToken(this.room);
752
818
  }
753
- const data = await this.request(`${this.apiBase}/token`, {
754
- method: "POST",
755
- body: JSON.stringify({
756
- room_code: this.room,
757
- user_id: this.participantId,
758
- display_name: options.displayName,
759
- avatar_url: options.avatarUrl
760
- })
761
- });
762
- this.token = data?.token;
763
- if (!this.token) throw new Error("Token endpoint returned no token");
764
- return this.connect(this.token, this.joinOptions);
765
- }
766
- async refreshAccessToken() {
767
- if (!this.room) throw new Error("Cannot refresh token without a room");
768
- const data = await this.request(`${this.apiBase}/token`, {
769
- method: "POST",
770
- body: JSON.stringify({
771
- room_code: this.room,
772
- user_id: this.participantId,
773
- display_name: this.joinOptions.displayName,
774
- avatar_url: this.joinOptions.avatarUrl
775
- })
776
- });
777
- if (!data?.token) throw new Error("Token refresh returned no token");
778
- this.token = data.token;
779
- return this.token;
819
+ return this.connect(this.authToken, this.joinOptions);
780
820
  }
781
821
  connect(token, options = {}) {
782
822
  this.token = token;
@@ -866,15 +906,9 @@ var SFUClient = class {
866
906
  this.reconnectBaseMs * Math.pow(2, this.reconnectAttempt - 1)
867
907
  );
868
908
  this.onReconnecting(this.reconnectAttempt);
869
- this.reconnectTimer = setTimeout(async () => {
909
+ this.reconnectTimer = setTimeout(() => {
870
910
  this.reconnectTimer = null;
871
- try {
872
- await this.refreshAccessToken();
873
- this.openWebSocket(this.joinOptions);
874
- } catch (error) {
875
- this.onError(error);
876
- this.scheduleReconnect();
877
- }
911
+ this.openWebSocket(this.joinOptions);
878
912
  }, delay);
879
913
  }
880
914
  failJoin(error) {
@@ -2355,6 +2389,8 @@ var VideoSDKCore = class {
2355
2389
  this.screenStream = null;
2356
2390
  this.isScreenSharing = false;
2357
2391
  this.participantName = "";
2392
+ this.cameraEnabled = false;
2393
+ this.cameraToggleInProgress = false;
2358
2394
  this.intentionalDisconnect = false;
2359
2395
  this.remoteStreams = /* @__PURE__ */ new Map();
2360
2396
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
@@ -2388,6 +2424,7 @@ var VideoSDKCore = class {
2388
2424
  this.participantName = name;
2389
2425
  try {
2390
2426
  this.localStream = await this.acquireOptionalLocalMedia();
2427
+ this.cameraEnabled = this.hasLiveCameraTrack();
2391
2428
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2392
2429
  if (localAudioTrack) {
2393
2430
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
@@ -2399,17 +2436,47 @@ var VideoSDKCore = class {
2399
2436
  throw err;
2400
2437
  }
2401
2438
  }
2402
- 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() {
2403
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);
2404
2470
  this.state.updateLocalParticipant({
2405
2471
  id: this.myId,
2406
2472
  name: this.participantName,
2407
2473
  media: {
2408
2474
  stream: this.localStream,
2409
- cameraTrack: this.localStream.getVideoTracks()[0],
2410
- audioTrack: this.localStream.getAudioTracks()[0],
2411
- micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? false,
2412
- camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? false,
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,
2413
2480
  isScreenSharing: this.isScreenSharing,
2414
2481
  screenStream: this.screenStream,
2415
2482
  screenTrack: this.screenStream?.getVideoTracks()[0]
@@ -2436,7 +2503,7 @@ var VideoSDKCore = class {
2436
2503
  async getMeetingSummary(roomCode, sessionId) {
2437
2504
  return this.sfuClient.getMeetingSummary(roomCode, sessionId);
2438
2505
  }
2439
- async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId) {
2506
+ async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId, includeVideo = true) {
2440
2507
  const combined = new MediaStream();
2441
2508
  if (!navigator.mediaDevices?.getUserMedia) {
2442
2509
  console.warn(
@@ -2444,13 +2511,13 @@ var VideoSDKCore = class {
2444
2511
  );
2445
2512
  return combined;
2446
2513
  }
2447
- const cameraPromise = navigator.mediaDevices.getUserMedia({
2514
+ const cameraPromise = includeVideo ? navigator.mediaDevices.getUserMedia({
2448
2515
  video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
2449
2516
  width: { ideal: 1280 },
2450
2517
  height: { ideal: 720 }
2451
2518
  },
2452
2519
  audio: false
2453
- });
2520
+ }) : Promise.resolve(null);
2454
2521
  const microphonePromise = navigator.mediaDevices.getUserMedia({
2455
2522
  video: false,
2456
2523
  audio: microphoneDeviceId ? {
@@ -2468,11 +2535,11 @@ var VideoSDKCore = class {
2468
2535
  cameraPromise,
2469
2536
  microphonePromise
2470
2537
  ]);
2471
- if (cameraResult.status === "fulfilled") {
2538
+ if (cameraResult.status === "fulfilled" && cameraResult.value) {
2472
2539
  for (const track of cameraResult.value.getVideoTracks()) {
2473
2540
  combined.addTrack(track);
2474
2541
  }
2475
- } else {
2542
+ } else if (cameraResult.status === "rejected") {
2476
2543
  console.warn("[SDK] Camera unavailable; continuing without video.", {
2477
2544
  name: cameraResult.reason?.name,
2478
2545
  message: cameraResult.reason?.message
@@ -2493,6 +2560,25 @@ var VideoSDKCore = class {
2493
2560
  }
2494
2561
  return combined;
2495
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
+ }
2496
2582
  /**
2497
2583
  * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
2498
2584
  * whenever possible (Join, first click inside the meeting, etc.).
@@ -2542,10 +2628,12 @@ var VideoSDKCore = class {
2542
2628
  this.participantName = name;
2543
2629
  this.room.id = roomId;
2544
2630
  this.room.name = name;
2631
+ this.cameraDeviceId = cameraDeviceId ?? this.cameraDeviceId;
2545
2632
  if (!this.localStream) {
2546
2633
  this.localStream = await this.acquireOptionalLocalMedia(
2547
- cameraDeviceId,
2548
- microphoneDeviceId
2634
+ this.cameraDeviceId,
2635
+ microphoneDeviceId,
2636
+ !videoMuted
2549
2637
  );
2550
2638
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2551
2639
  if (localAudioTrack) {
@@ -2555,10 +2643,17 @@ var VideoSDKCore = class {
2555
2643
  this.localStream.getAudioTracks().forEach((track) => {
2556
2644
  track.enabled = !audioMuted;
2557
2645
  });
2558
- this.localStream.getVideoTracks().forEach((track) => {
2559
- track.enabled = !videoMuted;
2560
- });
2561
- 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();
2562
2657
  const promise = this.connect(roomId, name);
2563
2658
  this.joinPromise = promise;
2564
2659
  try {
@@ -2588,7 +2683,7 @@ var VideoSDKCore = class {
2588
2683
  );
2589
2684
  }
2590
2685
  this.sfuClient.setLocalStream(this.localStream);
2591
- await this.sfuClient.getToken(roomId, {
2686
+ await this.sfuClient.join(roomId, {
2592
2687
  displayName: name,
2593
2688
  participantId: this.myId,
2594
2689
  audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? false),
@@ -3185,7 +3280,12 @@ var VideoSDKCore = class {
3185
3280
  if (!this.sfuClient) {
3186
3281
  throw new Error("SFU client is not initialized");
3187
3282
  }
3283
+ if (!this.cameraEnabled) {
3284
+ throw new Error("Your camera is off. Turn it on before switching devices.");
3285
+ }
3188
3286
  const track = await this.sfuClient.selectCamera(deviceId);
3287
+ this.cameraDeviceId = deviceId;
3288
+ this.cameraEnabled = true;
3189
3289
  if (this.localStream) {
3190
3290
  this.state.localStream = this.localStream;
3191
3291
  this.updateLocalMedia();
@@ -3227,24 +3327,52 @@ var VideoSDKCore = class {
3227
3327
  this.sfuClient?.muteAudio(!nextEnabled);
3228
3328
  this.events.onMicToggled?.(this.myId, nextEnabled);
3229
3329
  }
3230
- toggleCam() {
3231
- const mediaState = this.state.localParticipant?.media;
3232
- if (!mediaState || !this.localStream) {
3233
- return;
3234
- }
3235
- const nextEnabled = !mediaState.camEnabled;
3236
- this.localStream.getVideoTracks().forEach((track) => {
3237
- track.enabled = nextEnabled;
3238
- });
3239
- this.state.updateLocalParticipant({
3240
- id: this.myId,
3241
- name: this.participantName,
3242
- media: {
3243
- 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;
3244
3343
  }
3245
- });
3246
- this.sfuClient?.muteVideo(!nextEnabled);
3247
- 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
+ }
3248
3376
  }
3249
3377
  // SCREEN SHARING
3250
3378
  async startScreenShare() {
@@ -3402,6 +3530,8 @@ var VideoSDKCore = class {
3402
3530
  this.localStream.getTracks().forEach((track) => track.stop());
3403
3531
  this.localStream = null;
3404
3532
  }
3533
+ this.cameraEnabled = false;
3534
+ this.cameraToggleInProgress = false;
3405
3535
  if (this.screenStream) {
3406
3536
  this.screenStream.getTracks().forEach((track) => track.stop());
3407
3537
  this.screenStream = null;