@defcomm/meetingsdk 0.2.2 → 0.2.4

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
@@ -349,6 +349,9 @@ var SFUClient = class {
349
349
  this.localStream = null;
350
350
  this.screenStream = null;
351
351
  this.videoSender = null;
352
+ this.audioMuted = false;
353
+ this.videoMuted = false;
354
+ this.activeRecording = null;
352
355
  this.audioSender = null;
353
356
  this.screenSender = null;
354
357
  this.audioTransceiver = null;
@@ -369,7 +372,7 @@ var SFUClient = class {
369
372
  this.joiningWithResume = false;
370
373
  this.reconnectTimer = null;
371
374
  this.reconnectAttempt = 0;
372
- this.maxReconnectAttempts = 8;
375
+ this.maxReconnectAttempts = 6;
373
376
  this.reconnectBaseMs = 750;
374
377
  this.joinWaiter = null;
375
378
  this.transportRecoveryInProgress = false;
@@ -392,6 +395,8 @@ var SFUClient = class {
392
395
  this.trackToSource = /* @__PURE__ */ new Map();
393
396
  this.localTrackIds = /* @__PURE__ */ new Map();
394
397
  this.subscriberStatsTimer = null;
398
+ this.roomSyncTimer = null;
399
+ this.roomSyncInFlight = false;
395
400
  this.heartbeatTimer = null;
396
401
  this.apiBase = options.apiBase || "https://api.example.com/api";
397
402
  this.wsBase = options.wsBase || "wss://api.example.com/ws";
@@ -430,10 +435,14 @@ var SFUClient = class {
430
435
  });
431
436
  this.onTrackStateChanged = options.onTrackStateChanged || (() => {
432
437
  });
438
+ this.onParticipantMediaState = options.onParticipantMediaState || (() => {
439
+ });
433
440
  this.onRecordingStarted = options.onRecordingStarted || (() => {
434
441
  });
435
442
  this.onRecordingStopped = options.onRecordingStopped || (() => {
436
443
  });
444
+ this.onRoomState = options.onRoomState || (() => {
445
+ });
437
446
  this.onError = options.onError || console.error;
438
447
  this.autoSubscribe = options.autoSubscribe !== false;
439
448
  }
@@ -455,6 +464,29 @@ var SFUClient = class {
455
464
  this.heartbeatTimer = null;
456
465
  }
457
466
  }
467
+ startRoomStateSync() {
468
+ this.stopRoomStateSync();
469
+ this.roomSyncTimer = setInterval(() => {
470
+ if (this.state !== "connected") return;
471
+ void this.syncRoomState();
472
+ }, 5e3);
473
+ }
474
+ stopRoomStateSync() {
475
+ if (this.roomSyncTimer) {
476
+ clearInterval(this.roomSyncTimer);
477
+ this.roomSyncTimer = null;
478
+ }
479
+ this.roomSyncInFlight = false;
480
+ }
481
+ async syncRoomState() {
482
+ if (this.roomSyncInFlight || !this.isSocketOpen()) return;
483
+ this.roomSyncInFlight = true;
484
+ try {
485
+ this.send({ type: "sync_room_state" });
486
+ } finally {
487
+ this.roomSyncInFlight = false;
488
+ }
489
+ }
458
490
  createId() {
459
491
  return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `id_${Math.random().toString(36).slice(2)}_${Date.now()}`;
460
492
  }
@@ -552,9 +584,13 @@ var SFUClient = class {
552
584
  this.resumeToken = null;
553
585
  }
554
586
  this.room = nextRoom;
587
+ this.audioMuted = Boolean(options.audioMuted);
588
+ this.videoMuted = Boolean(options.videoMuted);
555
589
  this.joinOptions = {
556
590
  displayName: options.displayName,
557
- avatarUrl: options.avatarUrl
591
+ avatarUrl: options.avatarUrl,
592
+ audioMuted: this.audioMuted,
593
+ videoMuted: this.videoMuted
558
594
  };
559
595
  if (options.participantId) this.participantId = options.participantId;
560
596
  if (!this.resumeToken) {
@@ -623,7 +659,9 @@ var SFUClient = class {
623
659
  room_code: this.room,
624
660
  resume_token: this.resumeToken,
625
661
  display_name: options.displayName,
626
- avatar_url: options.avatarUrl
662
+ avatar_url: options.avatarUrl,
663
+ audio_muted: this.audioMuted,
664
+ video_muted: this.videoMuted
627
665
  };
628
666
  if (this.pendingJoinRequestId) {
629
667
  this.send({
@@ -664,8 +702,20 @@ var SFUClient = class {
664
702
  scheduleReconnect() {
665
703
  if (this.reconnectTimer || this.intentionalClose) return;
666
704
  if (this.reconnectAttempt >= this.maxReconnectAttempts) {
705
+ const error = new Error("SFU reconnect attempts exhausted");
706
+ console.error("[SFU] Reconnect attempts exhausted; leaving meeting", error);
707
+ this.intentionalClose = true;
708
+ this.stopHeartbeat();
709
+ this.stopRoomStateSync();
710
+ this.cleanupAll();
667
711
  this.setState("disconnected");
668
- this.failJoin(new Error("SFU reconnect attempts exhausted"));
712
+ this.onError({
713
+ code: "RECONNECT_TIMEOUT",
714
+ message: error.message,
715
+ recoverable: false
716
+ });
717
+ this.onDisconnected();
718
+ this.failJoin(error);
669
719
  return;
670
720
  }
671
721
  this.reconnectAttempt += 1;
@@ -737,7 +787,9 @@ var SFUClient = class {
737
787
  room_code: this.room,
738
788
  resume_token: this.resumeToken,
739
789
  display_name: this.joinOptions.displayName,
740
- avatar_url: this.joinOptions.avatarUrl
790
+ avatar_url: this.joinOptions.avatarUrl,
791
+ audio_muted: this.audioMuted,
792
+ video_muted: this.videoMuted
741
793
  }
742
794
  });
743
795
  }
@@ -812,22 +864,8 @@ var SFUClient = class {
812
864
  if (!track?.track_id) break;
813
865
  this.rememberTrack(track);
814
866
  if (track.participant_id === this.currentParticipantId) {
815
- if (track.source === "audio") {
816
- this.localTrackIds.set("audio", track.track_id);
817
- const muted = !(this.localStream?.getAudioTracks()[0]?.enabled ?? true);
818
- this.send({
819
- type: "track_state",
820
- payload: { track_id: track.track_id, muted }
821
- });
822
- }
823
- if (track.source === "camera") {
824
- this.localTrackIds.set("camera", track.track_id);
825
- const muted = !(this.localStream?.getVideoTracks()[0]?.enabled ?? true);
826
- this.send({
827
- type: "track_state",
828
- payload: { track_id: track.track_id, muted }
829
- });
830
- }
867
+ if (track.source === "audio") this.localTrackIds.set("audio", track.track_id);
868
+ if (track.source === "camera") this.localTrackIds.set("camera", track.track_id);
831
869
  if (track.source === "screen")
832
870
  this.localTrackIds.set("screen", track.track_id);
833
871
  break;
@@ -856,6 +894,27 @@ var SFUClient = class {
856
894
  this.onTrackUnpublished(track);
857
895
  break;
858
896
  }
897
+ case "participant_media_state": {
898
+ const participantId = payload.participant_id;
899
+ if (!participantId) break;
900
+ const participant = this.participants.get(participantId);
901
+ if (participant) {
902
+ participant.audio_muted = Boolean(payload.audio_muted);
903
+ participant.video_muted = Boolean(payload.video_muted);
904
+ this.participants.set(participantId, participant);
905
+ }
906
+ for (const track of this.trackMetadata.values()) {
907
+ if (track.participant_id !== participantId) continue;
908
+ if (track.source === "audio") track.muted = Boolean(payload.audio_muted);
909
+ if (track.source === "camera") track.muted = Boolean(payload.video_muted);
910
+ }
911
+ this.onParticipantMediaState(
912
+ participantId,
913
+ Boolean(payload.audio_muted),
914
+ Boolean(payload.video_muted)
915
+ );
916
+ break;
917
+ }
859
918
  case "track_state_changed": {
860
919
  const trackId = payload.track_id || payload.track?.track_id;
861
920
  if (!trackId) break;
@@ -877,14 +936,20 @@ var SFUClient = class {
877
936
  case "subscription_updated": {
878
937
  break;
879
938
  }
939
+ case "room_state": {
940
+ await this.applyRoomState(payload);
941
+ break;
942
+ }
880
943
  case "recording_started": {
881
- this.onRecordingStarted({
944
+ this.activeRecording = {
882
945
  recording_id: payload.recording_id,
883
946
  started_at: payload.started_at
884
- });
947
+ };
948
+ this.onRecordingStarted(this.activeRecording);
885
949
  break;
886
950
  }
887
951
  case "recording_stopped": {
952
+ this.activeRecording = null;
888
953
  this.onRecordingStopped({
889
954
  recording_id: payload.recording_id,
890
955
  stopped_at: payload.stopped_at
@@ -942,6 +1007,67 @@ var SFUClient = class {
942
1007
  console.debug("[SFU] Ignoring unknown server message", message);
943
1008
  }
944
1009
  }
1010
+ async applyRoomState(payload) {
1011
+ const authoritative = /* @__PURE__ */ new Map();
1012
+ for (const participant of payload?.participants || []) {
1013
+ if (participant?.participant_id) authoritative.set(participant.participant_id, participant);
1014
+ }
1015
+ for (const id of [...this.participants.keys()]) {
1016
+ if (!authoritative.has(id) && id !== this.currentParticipantId) {
1017
+ this.participants.delete(id);
1018
+ this.removeRemoteParticipant(id);
1019
+ this.onParticipantLeft(id);
1020
+ }
1021
+ }
1022
+ for (const [id, participant] of authoritative) {
1023
+ if (id === this.currentParticipantId) continue;
1024
+ const previous = this.participants.get(id);
1025
+ this.participants.set(id, {
1026
+ ...previous,
1027
+ ...participant,
1028
+ audio_muted: Boolean(participant.audio_muted),
1029
+ video_muted: Boolean(participant.video_muted)
1030
+ });
1031
+ if (!previous) this.onParticipantJoined(participant);
1032
+ if (participant.presence) this.onParticipantPresence(id, String(participant.presence));
1033
+ }
1034
+ const nextTracks = /* @__PURE__ */ new Map();
1035
+ for (const track of payload?.tracks || []) {
1036
+ if (!track?.track_id) continue;
1037
+ nextTracks.set(track.track_id, track);
1038
+ this.rememberTrack(track);
1039
+ }
1040
+ for (const id of [...this.trackMetadata.keys()]) {
1041
+ if (!nextTracks.has(id)) {
1042
+ this.trackMetadata.delete(id);
1043
+ this.subscribedTracks.delete(id);
1044
+ this.trackToParticipant.delete(id);
1045
+ this.trackToSource.delete(id);
1046
+ }
1047
+ }
1048
+ if (payload?.recording) {
1049
+ const current = {
1050
+ recording_id: payload.recording.recording_id,
1051
+ started_at: payload.recording.started_at
1052
+ };
1053
+ if (!this.activeRecording || this.activeRecording.recording_id !== current.recording_id) {
1054
+ this.activeRecording = current;
1055
+ this.onRecordingStarted(current);
1056
+ }
1057
+ } else if (this.activeRecording) {
1058
+ const previous = this.activeRecording;
1059
+ this.activeRecording = null;
1060
+ this.onRecordingStopped({
1061
+ recording_id: previous.recording_id,
1062
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString()
1063
+ });
1064
+ }
1065
+ if (this.autoSubscribe) {
1066
+ const missing = [...nextTracks.values()].filter((t) => t.participant_id !== this.currentParticipantId && !this.subscribedTracks.has(t.track_id)).map((t) => t.track_id);
1067
+ if (missing.length) await this.subscribeMultiple(missing);
1068
+ }
1069
+ this.onRoomState(payload);
1070
+ }
945
1071
  rememberTrack(track) {
946
1072
  this.trackMetadata.set(track.track_id, track);
947
1073
  this.trackToParticipant.set(track.track_id, track.participant_id);
@@ -971,8 +1097,23 @@ var SFUClient = class {
971
1097
  for (const track of payload.tracks || []) {
972
1098
  if (track?.track_id) this.rememberTrack(track);
973
1099
  }
1100
+ if (payload.recording) {
1101
+ this.activeRecording = {
1102
+ recording_id: payload.recording.recording_id,
1103
+ started_at: payload.recording.started_at
1104
+ };
1105
+ this.onRecordingStarted(this.activeRecording);
1106
+ } else if (this.activeRecording) {
1107
+ const previous = this.activeRecording;
1108
+ this.activeRecording = null;
1109
+ this.onRecordingStopped({
1110
+ recording_id: previous.recording_id,
1111
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString()
1112
+ });
1113
+ }
974
1114
  await this.initializePeerConnections();
975
1115
  this.setState("connected");
1116
+ this.startRoomStateSync();
976
1117
  this.onConnected(payload, resumed);
977
1118
  for (const participant of this.participants.values()) {
978
1119
  if (participant.participant_id !== this.currentParticipantId) {
@@ -1527,6 +1668,7 @@ var SFUClient = class {
1527
1668
  }
1528
1669
  async leave() {
1529
1670
  this.stopHeartbeat();
1671
+ this.stopRoomStateSync();
1530
1672
  this.intentionalClose = true;
1531
1673
  this.setState("leaving");
1532
1674
  if (this.reconnectTimer) {
@@ -1894,9 +2036,13 @@ var VideoSDKCore = class {
1894
2036
  );
1895
2037
  return;
1896
2038
  }
1897
- throw new Error(
1898
- `SFU client already exists in state "${state}". Disconnect explicitly before creating another connection.`
1899
- );
2039
+ if (state === "disconnected" || state === "idle") {
2040
+ this.sfuClient = null;
2041
+ } else {
2042
+ throw new Error(
2043
+ `SFU client already exists in state "${state}". Disconnect explicitly before creating another connection.`
2044
+ );
2045
+ }
1900
2046
  }
1901
2047
  const client = new SFUClient({
1902
2048
  apiBase: this.apiBase,
@@ -1914,6 +2060,48 @@ var VideoSDKCore = class {
1914
2060
  name: this.participantName
1915
2061
  });
1916
2062
  },
2063
+ onRoomState: (roomState) => {
2064
+ for (const participant of roomState?.participants || []) {
2065
+ const pid = participant?.participant_id;
2066
+ if (!pid || pid === this.myId) continue;
2067
+ const current = this.state.getParticipant(pid);
2068
+ if (!current) continue;
2069
+ this.state.updateParticipantMedia(pid, {
2070
+ stream: current.media?.stream ?? null,
2071
+ micEnabled: !Boolean(participant.audio_muted),
2072
+ camEnabled: !Boolean(participant.video_muted),
2073
+ isScreenSharing: Boolean(participant.screen_sharing)
2074
+ });
2075
+ }
2076
+ const presenters = /* @__PURE__ */ new Set();
2077
+ for (const participant of roomState?.participants || []) {
2078
+ const pid = participant?.participant_id;
2079
+ if (!pid || pid === this.myId) continue;
2080
+ const current = this.state.getParticipant(pid);
2081
+ if (!current) continue;
2082
+ if (participant.screen_sharing) {
2083
+ presenters.add(pid);
2084
+ this.state.updateParticipantMedia(pid, {
2085
+ isScreenSharing: true,
2086
+ screenStream: current.media?.screenStream ?? null
2087
+ });
2088
+ } else if (current.media?.isScreenSharing) {
2089
+ const screen = this.remoteScreenStreams.get(pid);
2090
+ screen?.getTracks().forEach((track) => track.stop());
2091
+ this.remoteScreenStreams.delete(pid);
2092
+ this.state.updateParticipantMedia(pid, {
2093
+ isScreenSharing: false,
2094
+ screenStream: null,
2095
+ screenTrack: void 0
2096
+ });
2097
+ }
2098
+ }
2099
+ const presenter = [...presenters][0] ?? null;
2100
+ if (this.state.presenterId !== presenter) {
2101
+ this.state.setPresenterId(presenter);
2102
+ }
2103
+ this.events.onRoomUpdate?.(roomState);
2104
+ },
1917
2105
  onDisconnected: () => {
1918
2106
  console.log("[SFU] Signaling WebSocket disconnected; reconnecting");
1919
2107
  },
@@ -1985,8 +2173,8 @@ var VideoSDKCore = class {
1985
2173
  name: pname,
1986
2174
  connectionState: "connected",
1987
2175
  media: {
1988
- micEnabled: true,
1989
- camEnabled: true,
2176
+ micEnabled: !Boolean(participant.audio_muted),
2177
+ camEnabled: !Boolean(participant.video_muted),
1990
2178
  isScreenSharing: false,
1991
2179
  stream: null
1992
2180
  }
@@ -2059,6 +2247,18 @@ var VideoSDKCore = class {
2059
2247
  camEnabled: track.kind === "video" ? false : current.media?.camEnabled ?? true
2060
2248
  });
2061
2249
  },
2250
+ onParticipantMediaState: (participantId, audioMuted, videoMuted) => {
2251
+ if (participantId === this.myId) return;
2252
+ const current = this.state.getParticipant(participantId);
2253
+ if (!current) return;
2254
+ this.state.updateParticipantMedia(participantId, {
2255
+ stream: current.media?.stream ?? null,
2256
+ micEnabled: !audioMuted,
2257
+ camEnabled: !videoMuted
2258
+ });
2259
+ this.events.onMuteStateChanged?.(participantId, "audio", audioMuted);
2260
+ this.events.onMuteStateChanged?.(participantId, "video", videoMuted);
2261
+ },
2062
2262
  onTrackStateChanged: (track) => {
2063
2263
  const pid = track.participant_id;
2064
2264
  if (!pid || pid === this.myId) {
@@ -2159,7 +2359,9 @@ var VideoSDKCore = class {
2159
2359
  client.setLocalStream(this.localStream);
2160
2360
  await client.getToken(roomId, {
2161
2361
  displayName: name,
2162
- participantId: this.myId
2362
+ participantId: this.myId,
2363
+ audioMuted: this.state.localParticipant?.media?.micEnabled,
2364
+ videoMuted: this.state.localParticipant?.media?.camEnabled
2163
2365
  });
2164
2366
  }
2165
2367
  // SFU STATE
@@ -2473,6 +2675,7 @@ var MeetingProvider = ({
2473
2675
  /* @__PURE__ */ new Set()
2474
2676
  );
2475
2677
  const meetingLeftListeners = useRef(/* @__PURE__ */ new Set());
2678
+ const roomUpdateListeners = useRef(/* @__PURE__ */ new Set());
2476
2679
  const trackStateListeners = useRef(/* @__PURE__ */ new Set());
2477
2680
  const recordingStartedListeners = useRef(/* @__PURE__ */ new Set());
2478
2681
  const recordingStoppedListeners = useRef(/* @__PURE__ */ new Set());
@@ -2497,6 +2700,9 @@ var MeetingProvider = ({
2497
2700
  onMeetingLeft: () => {
2498
2701
  meetingLeftListeners.current.forEach((fn) => fn());
2499
2702
  },
2703
+ onRoomUpdate: (data) => {
2704
+ roomUpdateListeners.current.forEach((fn) => fn(data));
2705
+ },
2500
2706
  onTrackStateChanged: (track) => {
2501
2707
  trackStateListeners.current.forEach((fn) => fn(track));
2502
2708
  },
@@ -2595,6 +2801,10 @@ var MeetingProvider = ({
2595
2801
  meetingLeftListeners.current.delete(cb);
2596
2802
  };
2597
2803
  },
2804
+ onRoomUpdate: (cb) => {
2805
+ roomUpdateListeners.current.add(cb);
2806
+ return () => roomUpdateListeners.current.delete(cb);
2807
+ },
2598
2808
  onTrackStateChanged: (cb) => {
2599
2809
  trackStateListeners.current.add(cb);
2600
2810
  return () => trackStateListeners.current.delete(cb);