@defcomm/meetingsdk 0.6.9 → 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
@@ -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,
@@ -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
  }
@@ -2324,6 +2389,8 @@ var VideoSDKCore = class {
2324
2389
  this.screenStream = null;
2325
2390
  this.isScreenSharing = false;
2326
2391
  this.participantName = "";
2392
+ this.cameraEnabled = false;
2393
+ this.cameraToggleInProgress = false;
2327
2394
  this.intentionalDisconnect = false;
2328
2395
  this.remoteStreams = /* @__PURE__ */ new Map();
2329
2396
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
@@ -2357,6 +2424,7 @@ var VideoSDKCore = class {
2357
2424
  this.participantName = name;
2358
2425
  try {
2359
2426
  this.localStream = await this.acquireOptionalLocalMedia();
2427
+ this.cameraEnabled = this.hasLiveCameraTrack();
2360
2428
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2361
2429
  if (localAudioTrack) {
2362
2430
  this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
@@ -2368,17 +2436,47 @@ var VideoSDKCore = class {
2368
2436
  throw err;
2369
2437
  }
2370
2438
  }
2371
- 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() {
2372
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);
2373
2470
  this.state.updateLocalParticipant({
2374
2471
  id: this.myId,
2375
2472
  name: this.participantName,
2376
2473
  media: {
2377
2474
  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,
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,
2382
2480
  isScreenSharing: this.isScreenSharing,
2383
2481
  screenStream: this.screenStream,
2384
2482
  screenTrack: this.screenStream?.getVideoTracks()[0]
@@ -2405,7 +2503,7 @@ var VideoSDKCore = class {
2405
2503
  async getMeetingSummary(roomCode, sessionId) {
2406
2504
  return this.sfuClient.getMeetingSummary(roomCode, sessionId);
2407
2505
  }
2408
- async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId) {
2506
+ async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId, includeVideo = true) {
2409
2507
  const combined = new MediaStream();
2410
2508
  if (!navigator.mediaDevices?.getUserMedia) {
2411
2509
  console.warn(
@@ -2413,13 +2511,13 @@ var VideoSDKCore = class {
2413
2511
  );
2414
2512
  return combined;
2415
2513
  }
2416
- const cameraPromise = navigator.mediaDevices.getUserMedia({
2514
+ const cameraPromise = includeVideo ? navigator.mediaDevices.getUserMedia({
2417
2515
  video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
2418
2516
  width: { ideal: 1280 },
2419
2517
  height: { ideal: 720 }
2420
2518
  },
2421
2519
  audio: false
2422
- });
2520
+ }) : Promise.resolve(null);
2423
2521
  const microphonePromise = navigator.mediaDevices.getUserMedia({
2424
2522
  video: false,
2425
2523
  audio: microphoneDeviceId ? {
@@ -2437,11 +2535,11 @@ var VideoSDKCore = class {
2437
2535
  cameraPromise,
2438
2536
  microphonePromise
2439
2537
  ]);
2440
- if (cameraResult.status === "fulfilled") {
2538
+ if (cameraResult.status === "fulfilled" && cameraResult.value) {
2441
2539
  for (const track of cameraResult.value.getVideoTracks()) {
2442
2540
  combined.addTrack(track);
2443
2541
  }
2444
- } else {
2542
+ } else if (cameraResult.status === "rejected") {
2445
2543
  console.warn("[SDK] Camera unavailable; continuing without video.", {
2446
2544
  name: cameraResult.reason?.name,
2447
2545
  message: cameraResult.reason?.message
@@ -2462,6 +2560,25 @@ var VideoSDKCore = class {
2462
2560
  }
2463
2561
  return combined;
2464
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
+ }
2465
2582
  /**
2466
2583
  * Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
2467
2584
  * whenever possible (Join, first click inside the meeting, etc.).
@@ -2511,10 +2628,12 @@ var VideoSDKCore = class {
2511
2628
  this.participantName = name;
2512
2629
  this.room.id = roomId;
2513
2630
  this.room.name = name;
2631
+ this.cameraDeviceId = cameraDeviceId ?? this.cameraDeviceId;
2514
2632
  if (!this.localStream) {
2515
2633
  this.localStream = await this.acquireOptionalLocalMedia(
2516
- cameraDeviceId,
2517
- microphoneDeviceId
2634
+ this.cameraDeviceId,
2635
+ microphoneDeviceId,
2636
+ !videoMuted
2518
2637
  );
2519
2638
  const localAudioTrack = this.localStream.getAudioTracks()[0];
2520
2639
  if (localAudioTrack) {
@@ -2524,10 +2643,17 @@ var VideoSDKCore = class {
2524
2643
  this.localStream.getAudioTracks().forEach((track) => {
2525
2644
  track.enabled = !audioMuted;
2526
2645
  });
2527
- this.localStream.getVideoTracks().forEach((track) => {
2528
- track.enabled = !videoMuted;
2529
- });
2530
- 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();
2531
2657
  const promise = this.connect(roomId, name);
2532
2658
  this.joinPromise = promise;
2533
2659
  try {
@@ -3154,7 +3280,12 @@ var VideoSDKCore = class {
3154
3280
  if (!this.sfuClient) {
3155
3281
  throw new Error("SFU client is not initialized");
3156
3282
  }
3283
+ if (!this.cameraEnabled) {
3284
+ throw new Error("Your camera is off. Turn it on before switching devices.");
3285
+ }
3157
3286
  const track = await this.sfuClient.selectCamera(deviceId);
3287
+ this.cameraDeviceId = deviceId;
3288
+ this.cameraEnabled = true;
3158
3289
  if (this.localStream) {
3159
3290
  this.state.localStream = this.localStream;
3160
3291
  this.updateLocalMedia();
@@ -3196,24 +3327,52 @@ var VideoSDKCore = class {
3196
3327
  this.sfuClient?.muteAudio(!nextEnabled);
3197
3328
  this.events.onMicToggled?.(this.myId, nextEnabled);
3198
3329
  }
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
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;
3213
3343
  }
3214
- });
3215
- this.sfuClient?.muteVideo(!nextEnabled);
3216
- 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
+ }
3217
3376
  }
3218
3377
  // SCREEN SHARING
3219
3378
  async startScreenShare() {
@@ -3371,6 +3530,8 @@ var VideoSDKCore = class {
3371
3530
  this.localStream.getTracks().forEach((track) => track.stop());
3372
3531
  this.localStream = null;
3373
3532
  }
3533
+ this.cameraEnabled = false;
3534
+ this.cameraToggleInProgress = false;
3374
3535
  if (this.screenStream) {
3375
3536
  this.screenStream.getTracks().forEach((track) => track.stop());
3376
3537
  this.screenStream = null;