@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/dist/index.mjs CHANGED
@@ -386,7 +386,7 @@ var MeetingState = class {
386
386
  const nextMedia = {
387
387
  stream: patch.media?.stream ?? prevMedia.stream,
388
388
  screenStream: patch.media?.screenStream ?? prevMedia.screenStream,
389
- cameraTrack: patch.media?.cameraTrack ?? prevMedia.cameraTrack,
389
+ cameraTrack: patch.media && Object.prototype.hasOwnProperty.call(patch.media, "cameraTrack") ? patch.media.cameraTrack ?? void 0 : prevMedia.cameraTrack,
390
390
  screenTrack: patch.media?.screenTrack ?? prevMedia.screenTrack,
391
391
  audioTrack: patch.media?.audioTrack ?? prevMedia.audioTrack,
392
392
  micEnabled: patch.media?.micEnabled ?? prevMedia.micEnabled,
@@ -473,9 +473,9 @@ var SFUClient = class {
473
473
  this.publisherRevision = 0;
474
474
  this.screenShareGeneration = 0;
475
475
  this.currentParticipantId = null;
476
- // Provider/application token used for authenticated REST calls.
476
+ // Token supplied by the application for REST and SFU authentication.
477
477
  this.authToken = null;
478
- // Room/SFU token returned by /token and used for the WebSocket.
478
+ // Token used for the current WebSocket session.
479
479
  this.token = null;
480
480
  this.room = null;
481
481
  this.resumeToken = null;
@@ -647,6 +647,71 @@ var SFUClient = class {
647
647
  setLocalStream(stream) {
648
648
  this.localStream = stream;
649
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
+ }
650
715
  nextRequestId() {
651
716
  return (++this.requestId).toString();
652
717
  }
@@ -701,7 +766,8 @@ var SFUClient = class {
701
766
  }
702
767
  );
703
768
  }
704
- async getToken(roomCode, options = {}) {
769
+ async join(roomCode, options = {}) {
770
+ if (!this.authToken) throw new Error("Authentication token is required");
705
771
  const nextRoom = roomCode.trim();
706
772
  if (this.room && this.room !== nextRoom) {
707
773
  this.resumeToken = null;
@@ -719,33 +785,7 @@ var SFUClient = class {
719
785
  if (!this.resumeToken) {
720
786
  this.resumeToken = this.loadResumeToken(this.room);
721
787
  }
722
- const data = await this.request(`${this.apiBase}/token`, {
723
- method: "POST",
724
- body: JSON.stringify({
725
- room_code: this.room,
726
- user_id: this.participantId,
727
- display_name: options.displayName,
728
- avatar_url: options.avatarUrl
729
- })
730
- });
731
- this.token = data?.token;
732
- if (!this.token) throw new Error("Token endpoint returned no token");
733
- return this.connect(this.token, this.joinOptions);
734
- }
735
- async refreshAccessToken() {
736
- if (!this.room) throw new Error("Cannot refresh token without a room");
737
- const data = await this.request(`${this.apiBase}/token`, {
738
- method: "POST",
739
- body: JSON.stringify({
740
- room_code: this.room,
741
- user_id: this.participantId,
742
- display_name: this.joinOptions.displayName,
743
- avatar_url: this.joinOptions.avatarUrl
744
- })
745
- });
746
- if (!data?.token) throw new Error("Token refresh returned no token");
747
- this.token = data.token;
748
- return this.token;
788
+ return this.connect(this.authToken, this.joinOptions);
749
789
  }
750
790
  connect(token, options = {}) {
751
791
  this.token = token;
@@ -835,15 +875,9 @@ var SFUClient = class {
835
875
  this.reconnectBaseMs * Math.pow(2, this.reconnectAttempt - 1)
836
876
  );
837
877
  this.onReconnecting(this.reconnectAttempt);
838
- this.reconnectTimer = setTimeout(async () => {
878
+ this.reconnectTimer = setTimeout(() => {
839
879
  this.reconnectTimer = null;
840
- try {
841
- await this.refreshAccessToken();
842
- this.openWebSocket(this.joinOptions);
843
- } catch (error) {
844
- this.onError(error);
845
- this.scheduleReconnect();
846
- }
880
+ this.openWebSocket(this.joinOptions);
847
881
  }, delay);
848
882
  }
849
883
  failJoin(error) {
@@ -2324,6 +2358,8 @@ var VideoSDKCore = class {
2324
2358
  this.screenStream = null;
2325
2359
  this.isScreenSharing = false;
2326
2360
  this.participantName = "";
2361
+ this.cameraEnabled = false;
2362
+ this.cameraToggleInProgress = false;
2327
2363
  this.intentionalDisconnect = false;
2328
2364
  this.remoteStreams = /* @__PURE__ */ new Map();
2329
2365
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
@@ -2357,6 +2393,7 @@ var VideoSDKCore = class {
2357
2393
  this.participantName = name;
2358
2394
  try {
2359
2395
  this.localStream = await this.acquireOptionalLocalMedia();
2396
+ this.cameraEnabled = this.hasLiveCameraTrack();
2360
2397
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2361
2398
  if (localAudioTrack) {
2362
2399
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
@@ -2368,17 +2405,47 @@ var VideoSDKCore = class {
2368
2405
  throw err;
2369
2406
  }
2370
2407
  }
2371
- updateLocalMedia(audioMuted = false, videoMuted = false) {
2408
+ hasLiveCameraTrack() {
2409
+ return Boolean(
2410
+ this.localStream?.getVideoTracks().some((track) => track.readyState === "live")
2411
+ );
2412
+ }
2413
+ getLiveCameraTrack() {
2414
+ return this.localStream?.getVideoTracks().find((track) => track.readyState === "live") ?? null;
2415
+ }
2416
+ /**
2417
+ * Stop every locally captured camera track and remove it from the stream.
2418
+ * Track.enabled=false only emits black frames; stop() releases camera capture.
2419
+ */
2420
+ stopCameraCapture() {
2421
+ if (!this.localStream) {
2422
+ this.cameraEnabled = false;
2423
+ return;
2424
+ }
2425
+ for (const track of this.localStream.getVideoTracks()) {
2426
+ this.localStream.removeTrack(track);
2427
+ try {
2428
+ track.stop();
2429
+ } catch {
2430
+ }
2431
+ }
2432
+ this.cameraEnabled = false;
2433
+ }
2434
+ updateLocalMedia() {
2372
2435
  if (!this.localStream) return;
2436
+ const cameraTrack = this.getLiveCameraTrack();
2437
+ const audioTrack = this.localStream.getAudioTracks().find((track) => track.readyState === "live");
2438
+ this.cameraEnabled = Boolean(cameraTrack && cameraTrack.enabled);
2373
2439
  this.state.updateLocalParticipant({
2374
2440
  id: this.myId,
2375
2441
  name: this.participantName,
2376
2442
  media: {
2377
2443
  stream: this.localStream,
2378
- cameraTrack: this.localStream.getVideoTracks()[0],
2379
- audioTrack: this.localStream.getAudioTracks()[0],
2380
- micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? false,
2381
- camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? false,
2444
+ // Explicit null clears a previously published track from React state.
2445
+ cameraTrack,
2446
+ audioTrack: audioTrack ?? void 0,
2447
+ micEnabled: audioTrack?.enabled ?? false,
2448
+ camEnabled: this.cameraEnabled,
2382
2449
  isScreenSharing: this.isScreenSharing,
2383
2450
  screenStream: this.screenStream,
2384
2451
  screenTrack: this.screenStream?.getVideoTracks()[0]
@@ -2405,7 +2472,7 @@ var VideoSDKCore = class {
2405
2472
  async getMeetingSummary(roomCode, sessionId) {
2406
2473
  return this.sfuClient.getMeetingSummary(roomCode, sessionId);
2407
2474
  }
2408
- async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId) {
2475
+ async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId, includeVideo = true) {
2409
2476
  const combined = new MediaStream();
2410
2477
  if (!navigator.mediaDevices?.getUserMedia) {
2411
2478
  console.warn(
@@ -2413,13 +2480,13 @@ var VideoSDKCore = class {
2413
2480
  );
2414
2481
  return combined;
2415
2482
  }
2416
- const cameraPromise = navigator.mediaDevices.getUserMedia({
2483
+ const cameraPromise = includeVideo ? navigator.mediaDevices.getUserMedia({
2417
2484
  video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
2418
2485
  width: { ideal: 1280 },
2419
2486
  height: { ideal: 720 }
2420
2487
  },
2421
2488
  audio: false
2422
- });
2489
+ }) : Promise.resolve(null);
2423
2490
  const microphonePromise = navigator.mediaDevices.getUserMedia({
2424
2491
  video: false,
2425
2492
  audio: microphoneDeviceId ? {
@@ -2437,11 +2504,11 @@ var VideoSDKCore = class {
2437
2504
  cameraPromise,
2438
2505
  microphonePromise
2439
2506
  ]);
2440
- if (cameraResult.status === "fulfilled") {
2507
+ if (cameraResult.status === "fulfilled" && cameraResult.value) {
2441
2508
  for (const track of cameraResult.value.getVideoTracks()) {
2442
2509
  combined.addTrack(track);
2443
2510
  }
2444
- } else {
2511
+ } else if (cameraResult.status === "rejected") {
2445
2512
  console.warn("[SDK] Camera unavailable; continuing without video.", {
2446
2513
  name: cameraResult.reason?.name,
2447
2514
  message: cameraResult.reason?.message
@@ -2462,6 +2529,25 @@ var VideoSDKCore = class {
2462
2529
  }
2463
2530
  return combined;
2464
2531
  }
2532
+ async acquireCameraTrack() {
2533
+ if (!navigator.mediaDevices?.getUserMedia) {
2534
+ throw new Error("Camera access is not supported on this device");
2535
+ }
2536
+ const stream = await navigator.mediaDevices.getUserMedia({
2537
+ video: this.cameraDeviceId ? { deviceId: { exact: this.cameraDeviceId } } : {
2538
+ width: { ideal: 1280 },
2539
+ height: { ideal: 720 }
2540
+ },
2541
+ audio: false
2542
+ });
2543
+ const track = stream.getVideoTracks()[0];
2544
+ if (!track) {
2545
+ stream.getTracks().forEach((item) => item.stop());
2546
+ throw new Error("The selected camera did not provide a video track");
2547
+ }
2548
+ this.cameraDeviceId = track.getSettings().deviceId || this.cameraDeviceId;
2549
+ return track;
2550
+ }
2465
2551
  /**
2466
2552
  * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
2467
2553
  * whenever possible (Join, first click inside the meeting, etc.).
@@ -2511,10 +2597,12 @@ var VideoSDKCore = class {
2511
2597
  this.participantName = name;
2512
2598
  this.room.id = roomId;
2513
2599
  this.room.name = name;
2600
+ this.cameraDeviceId = cameraDeviceId ?? this.cameraDeviceId;
2514
2601
  if (!this.localStream) {
2515
2602
  this.localStream = await this.acquireOptionalLocalMedia(
2516
- cameraDeviceId,
2517
- microphoneDeviceId
2603
+ this.cameraDeviceId,
2604
+ microphoneDeviceId,
2605
+ !videoMuted
2518
2606
  );
2519
2607
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2520
2608
  if (localAudioTrack) {
@@ -2524,10 +2612,17 @@ var VideoSDKCore = class {
2524
2612
  this.localStream.getAudioTracks().forEach((track) => {
2525
2613
  track.enabled = !audioMuted;
2526
2614
  });
2527
- this.localStream.getVideoTracks().forEach((track) => {
2528
- track.enabled = !videoMuted;
2529
- });
2530
- this.updateLocalMedia(audioMuted, videoMuted);
2615
+ if (videoMuted) {
2616
+ this.stopCameraCapture();
2617
+ } else {
2618
+ const initialCameraTrack = this.getLiveCameraTrack();
2619
+ this.cameraEnabled = Boolean(initialCameraTrack);
2620
+ if (initialCameraTrack) {
2621
+ this.cameraDeviceId = initialCameraTrack.getSettings().deviceId || this.cameraDeviceId;
2622
+ initialCameraTrack.enabled = true;
2623
+ }
2624
+ }
2625
+ this.updateLocalMedia();
2531
2626
  const promise = this.connect(roomId, name);
2532
2627
  this.joinPromise = promise;
2533
2628
  try {
@@ -2557,7 +2652,7 @@ var VideoSDKCore = class {
2557
2652
  );
2558
2653
  }
2559
2654
  this.sfuClient.setLocalStream(this.localStream);
2560
- await this.sfuClient.getToken(roomId, {
2655
+ await this.sfuClient.join(roomId, {
2561
2656
  displayName: name,
2562
2657
  participantId: this.myId,
2563
2658
  audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? false),
@@ -3154,7 +3249,12 @@ var VideoSDKCore = class {
3154
3249
  if (!this.sfuClient) {
3155
3250
  throw new Error("SFU client is not initialized");
3156
3251
  }
3252
+ if (!this.cameraEnabled) {
3253
+ throw new Error("Your camera is off. Turn it on before switching devices.");
3254
+ }
3157
3255
  const track = await this.sfuClient.selectCamera(deviceId);
3256
+ this.cameraDeviceId = deviceId;
3257
+ this.cameraEnabled = true;
3158
3258
  if (this.localStream) {
3159
3259
  this.state.localStream = this.localStream;
3160
3260
  this.updateLocalMedia();
@@ -3196,24 +3296,52 @@ var VideoSDKCore = class {
3196
3296
  this.sfuClient?.muteAudio(!nextEnabled);
3197
3297
  this.events.onMicToggled?.(this.myId, nextEnabled);
3198
3298
  }
3199
- toggleCam() {
3200
- const mediaState = this.state.localParticipant?.media;
3201
- if (!mediaState || !this.localStream) {
3202
- return;
3203
- }
3204
- const nextEnabled = !mediaState.camEnabled;
3205
- this.localStream.getVideoTracks().forEach((track) => {
3206
- track.enabled = nextEnabled;
3207
- });
3208
- this.state.updateLocalParticipant({
3209
- id: this.myId,
3210
- name: this.participantName,
3211
- media: {
3212
- camEnabled: nextEnabled
3299
+ async toggleCam() {
3300
+ if (!this.state.localParticipant?.media || !this.localStream) return;
3301
+ if (this.cameraToggleInProgress) return;
3302
+ this.cameraToggleInProgress = true;
3303
+ const nextEnabled = !this.cameraEnabled;
3304
+ try {
3305
+ if (!nextEnabled) {
3306
+ this.sfuClient?.muteVideo(true);
3307
+ this.stopCameraCapture();
3308
+ await this.sfuClient?.setCameraTrack(null);
3309
+ this.updateLocalMedia();
3310
+ this.events.onCamToggled?.(this.myId, false);
3311
+ return;
3213
3312
  }
3214
- });
3215
- this.sfuClient?.muteVideo(!nextEnabled);
3216
- this.events.onCamToggled?.(this.myId, nextEnabled);
3313
+ const track = await this.acquireCameraTrack();
3314
+ if (!this.localStream) this.localStream = new MediaStream();
3315
+ this.localStream.addTrack(track);
3316
+ try {
3317
+ await this.sfuClient?.setCameraTrack(track);
3318
+ this.cameraEnabled = true;
3319
+ this.sfuClient?.muteVideo(false);
3320
+ this.updateLocalMedia();
3321
+ this.events.onCamToggled?.(this.myId, true);
3322
+ } catch (error) {
3323
+ this.localStream.removeTrack(track);
3324
+ try {
3325
+ track.stop();
3326
+ } catch {
3327
+ }
3328
+ this.cameraEnabled = false;
3329
+ this.updateLocalMedia();
3330
+ throw error;
3331
+ }
3332
+ } catch (error) {
3333
+ this.cameraEnabled = false;
3334
+ this.updateLocalMedia();
3335
+ this.emitError(
3336
+ "CAMERA_TOGGLE_FAILED",
3337
+ error?.message || "Unable to change camera state",
3338
+ error,
3339
+ true
3340
+ );
3341
+ throw error;
3342
+ } finally {
3343
+ this.cameraToggleInProgress = false;
3344
+ }
3217
3345
  }
3218
3346
  // SCREEN SHARING
3219
3347
  async startScreenShare() {
@@ -3371,6 +3499,8 @@ var VideoSDKCore = class {
3371
3499
  this.localStream.getTracks().forEach((track) => track.stop());
3372
3500
  this.localStream = null;
3373
3501
  }
3502
+ this.cameraEnabled = false;
3503
+ this.cameraToggleInProgress = false;
3374
3504
  if (this.screenStream) {
3375
3505
  this.screenStream.getTracks().forEach((track) => track.stop());
3376
3506
  this.screenStream = null;