@defcomm/meetingsdk 0.2.3 → 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.d.mts CHANGED
@@ -272,6 +272,7 @@ interface SFUClientOptions {
272
272
  onScreenShareStarted?: (participantId: string, track?: TrackInfo) => void;
273
273
  onScreenShareStopped?: (participantId: string, trackId?: string) => void;
274
274
  onTrackStateChanged?: (track: TrackInfo) => void;
275
+ onParticipantMediaState?: (participantId: string, audioMuted: boolean, videoMuted: boolean) => void;
275
276
  onRecordingStarted?: (recording: {
276
277
  recording_id: string;
277
278
  started_at: string;
@@ -295,6 +296,9 @@ declare class SFUClient {
295
296
  private localStream;
296
297
  private screenStream;
297
298
  private videoSender;
299
+ private audioMuted;
300
+ private videoMuted;
301
+ private activeRecording;
298
302
  private audioSender;
299
303
  private screenSender;
300
304
  private audioTransceiver;
@@ -354,6 +358,7 @@ declare class SFUClient {
354
358
  private onScreenShareStarted;
355
359
  private onScreenShareStopped;
356
360
  private onTrackStateChanged;
361
+ private onParticipantMediaState;
357
362
  private onRecordingStarted;
358
363
  private onRecordingStopped;
359
364
  private onRoomState;
@@ -392,6 +397,8 @@ declare class SFUClient {
392
397
  displayName?: string;
393
398
  avatarUrl?: string;
394
399
  participantId?: string;
400
+ audioMuted?: boolean;
401
+ videoMuted?: boolean;
395
402
  }): Promise<void>;
396
403
  private refreshAccessToken;
397
404
  private connect;
package/dist/index.d.ts CHANGED
@@ -272,6 +272,7 @@ interface SFUClientOptions {
272
272
  onScreenShareStarted?: (participantId: string, track?: TrackInfo) => void;
273
273
  onScreenShareStopped?: (participantId: string, trackId?: string) => void;
274
274
  onTrackStateChanged?: (track: TrackInfo) => void;
275
+ onParticipantMediaState?: (participantId: string, audioMuted: boolean, videoMuted: boolean) => void;
275
276
  onRecordingStarted?: (recording: {
276
277
  recording_id: string;
277
278
  started_at: string;
@@ -295,6 +296,9 @@ declare class SFUClient {
295
296
  private localStream;
296
297
  private screenStream;
297
298
  private videoSender;
299
+ private audioMuted;
300
+ private videoMuted;
301
+ private activeRecording;
298
302
  private audioSender;
299
303
  private screenSender;
300
304
  private audioTransceiver;
@@ -354,6 +358,7 @@ declare class SFUClient {
354
358
  private onScreenShareStarted;
355
359
  private onScreenShareStopped;
356
360
  private onTrackStateChanged;
361
+ private onParticipantMediaState;
357
362
  private onRecordingStarted;
358
363
  private onRecordingStopped;
359
364
  private onRoomState;
@@ -392,6 +397,8 @@ declare class SFUClient {
392
397
  displayName?: string;
393
398
  avatarUrl?: string;
394
399
  participantId?: string;
400
+ audioMuted?: boolean;
401
+ videoMuted?: boolean;
395
402
  }): Promise<void>;
396
403
  private refreshAccessToken;
397
404
  private connect;
package/dist/index.js CHANGED
@@ -385,6 +385,9 @@ var SFUClient = class {
385
385
  this.localStream = null;
386
386
  this.screenStream = null;
387
387
  this.videoSender = null;
388
+ this.audioMuted = false;
389
+ this.videoMuted = false;
390
+ this.activeRecording = null;
388
391
  this.audioSender = null;
389
392
  this.screenSender = null;
390
393
  this.audioTransceiver = null;
@@ -405,7 +408,7 @@ var SFUClient = class {
405
408
  this.joiningWithResume = false;
406
409
  this.reconnectTimer = null;
407
410
  this.reconnectAttempt = 0;
408
- this.maxReconnectAttempts = 8;
411
+ this.maxReconnectAttempts = 6;
409
412
  this.reconnectBaseMs = 750;
410
413
  this.joinWaiter = null;
411
414
  this.transportRecoveryInProgress = false;
@@ -468,6 +471,8 @@ var SFUClient = class {
468
471
  });
469
472
  this.onTrackStateChanged = options.onTrackStateChanged || (() => {
470
473
  });
474
+ this.onParticipantMediaState = options.onParticipantMediaState || (() => {
475
+ });
471
476
  this.onRecordingStarted = options.onRecordingStarted || (() => {
472
477
  });
473
478
  this.onRecordingStopped = options.onRecordingStopped || (() => {
@@ -615,9 +620,13 @@ var SFUClient = class {
615
620
  this.resumeToken = null;
616
621
  }
617
622
  this.room = nextRoom;
623
+ this.audioMuted = Boolean(options.audioMuted);
624
+ this.videoMuted = Boolean(options.videoMuted);
618
625
  this.joinOptions = {
619
626
  displayName: options.displayName,
620
- avatarUrl: options.avatarUrl
627
+ avatarUrl: options.avatarUrl,
628
+ audioMuted: this.audioMuted,
629
+ videoMuted: this.videoMuted
621
630
  };
622
631
  if (options.participantId) this.participantId = options.participantId;
623
632
  if (!this.resumeToken) {
@@ -686,7 +695,9 @@ var SFUClient = class {
686
695
  room_code: this.room,
687
696
  resume_token: this.resumeToken,
688
697
  display_name: options.displayName,
689
- avatar_url: options.avatarUrl
698
+ avatar_url: options.avatarUrl,
699
+ audio_muted: this.audioMuted,
700
+ video_muted: this.videoMuted
690
701
  };
691
702
  if (this.pendingJoinRequestId) {
692
703
  this.send({
@@ -727,8 +738,20 @@ var SFUClient = class {
727
738
  scheduleReconnect() {
728
739
  if (this.reconnectTimer || this.intentionalClose) return;
729
740
  if (this.reconnectAttempt >= this.maxReconnectAttempts) {
741
+ const error = new Error("SFU reconnect attempts exhausted");
742
+ console.error("[SFU] Reconnect attempts exhausted; leaving meeting", error);
743
+ this.intentionalClose = true;
744
+ this.stopHeartbeat();
745
+ this.stopRoomStateSync();
746
+ this.cleanupAll();
730
747
  this.setState("disconnected");
731
- this.failJoin(new Error("SFU reconnect attempts exhausted"));
748
+ this.onError({
749
+ code: "RECONNECT_TIMEOUT",
750
+ message: error.message,
751
+ recoverable: false
752
+ });
753
+ this.onDisconnected();
754
+ this.failJoin(error);
732
755
  return;
733
756
  }
734
757
  this.reconnectAttempt += 1;
@@ -800,7 +823,9 @@ var SFUClient = class {
800
823
  room_code: this.room,
801
824
  resume_token: this.resumeToken,
802
825
  display_name: this.joinOptions.displayName,
803
- avatar_url: this.joinOptions.avatarUrl
826
+ avatar_url: this.joinOptions.avatarUrl,
827
+ audio_muted: this.audioMuted,
828
+ video_muted: this.videoMuted
804
829
  }
805
830
  });
806
831
  }
@@ -875,22 +900,8 @@ var SFUClient = class {
875
900
  if (!track?.track_id) break;
876
901
  this.rememberTrack(track);
877
902
  if (track.participant_id === this.currentParticipantId) {
878
- if (track.source === "audio") {
879
- this.localTrackIds.set("audio", track.track_id);
880
- const muted = !(this.localStream?.getAudioTracks()[0]?.enabled ?? true);
881
- this.send({
882
- type: "track_state",
883
- payload: { track_id: track.track_id, muted }
884
- });
885
- }
886
- if (track.source === "camera") {
887
- this.localTrackIds.set("camera", track.track_id);
888
- const muted = !(this.localStream?.getVideoTracks()[0]?.enabled ?? true);
889
- this.send({
890
- type: "track_state",
891
- payload: { track_id: track.track_id, muted }
892
- });
893
- }
903
+ if (track.source === "audio") this.localTrackIds.set("audio", track.track_id);
904
+ if (track.source === "camera") this.localTrackIds.set("camera", track.track_id);
894
905
  if (track.source === "screen")
895
906
  this.localTrackIds.set("screen", track.track_id);
896
907
  break;
@@ -919,6 +930,27 @@ var SFUClient = class {
919
930
  this.onTrackUnpublished(track);
920
931
  break;
921
932
  }
933
+ case "participant_media_state": {
934
+ const participantId = payload.participant_id;
935
+ if (!participantId) break;
936
+ const participant = this.participants.get(participantId);
937
+ if (participant) {
938
+ participant.audio_muted = Boolean(payload.audio_muted);
939
+ participant.video_muted = Boolean(payload.video_muted);
940
+ this.participants.set(participantId, participant);
941
+ }
942
+ for (const track of this.trackMetadata.values()) {
943
+ if (track.participant_id !== participantId) continue;
944
+ if (track.source === "audio") track.muted = Boolean(payload.audio_muted);
945
+ if (track.source === "camera") track.muted = Boolean(payload.video_muted);
946
+ }
947
+ this.onParticipantMediaState(
948
+ participantId,
949
+ Boolean(payload.audio_muted),
950
+ Boolean(payload.video_muted)
951
+ );
952
+ break;
953
+ }
922
954
  case "track_state_changed": {
923
955
  const trackId = payload.track_id || payload.track?.track_id;
924
956
  if (!trackId) break;
@@ -945,13 +977,15 @@ var SFUClient = class {
945
977
  break;
946
978
  }
947
979
  case "recording_started": {
948
- this.onRecordingStarted({
980
+ this.activeRecording = {
949
981
  recording_id: payload.recording_id,
950
982
  started_at: payload.started_at
951
- });
983
+ };
984
+ this.onRecordingStarted(this.activeRecording);
952
985
  break;
953
986
  }
954
987
  case "recording_stopped": {
988
+ this.activeRecording = null;
955
989
  this.onRecordingStopped({
956
990
  recording_id: payload.recording_id,
957
991
  stopped_at: payload.stopped_at
@@ -1010,68 +1044,63 @@ var SFUClient = class {
1010
1044
  }
1011
1045
  }
1012
1046
  async applyRoomState(payload) {
1013
- const nextParticipants = /* @__PURE__ */ new Map();
1014
- for (const participant of payload.participants || []) {
1015
- const id = participant?.participant_id;
1016
- if (!id) continue;
1017
- nextParticipants.set(id, participant);
1047
+ const authoritative = /* @__PURE__ */ new Map();
1048
+ for (const participant of payload?.participants || []) {
1049
+ if (participant?.participant_id) authoritative.set(participant.participant_id, participant);
1018
1050
  }
1019
- for (const [id] of this.participants) {
1020
- if (id === this.currentParticipantId) continue;
1021
- if (!nextParticipants.has(id)) {
1051
+ for (const id of [...this.participants.keys()]) {
1052
+ if (!authoritative.has(id) && id !== this.currentParticipantId) {
1022
1053
  this.participants.delete(id);
1023
1054
  this.removeRemoteParticipant(id);
1024
1055
  this.onParticipantLeft(id);
1025
1056
  }
1026
1057
  }
1027
- for (const [id, participant] of nextParticipants) {
1058
+ for (const [id, participant] of authoritative) {
1028
1059
  if (id === this.currentParticipantId) continue;
1029
- const existed = this.participants.has(id);
1030
- this.participants.set(id, participant);
1031
- if (!existed) {
1032
- this.onParticipantJoined(participant);
1033
- }
1034
- const sharing = Boolean(participant.screen_sharing);
1035
- const screenTrackId = participant.screen_track_id ? String(participant.screen_track_id) : void 0;
1036
- if (sharing) {
1037
- const track = screenTrackId ? (payload.tracks || []).find(
1038
- (candidate) => String(candidate.track_id) === screenTrackId
1039
- ) : void 0;
1040
- this.onScreenShareStarted(id, track);
1041
- } else if (this.remoteScreenStreams.has(id)) {
1042
- this.remoteScreenStreams.get(id)?.getTracks().forEach((track) => track.stop());
1043
- this.remoteScreenStreams.delete(id);
1044
- this.onScreenShareStopped(id, screenTrackId);
1045
- }
1060
+ const previous = this.participants.get(id);
1061
+ this.participants.set(id, {
1062
+ ...previous,
1063
+ ...participant,
1064
+ audio_muted: Boolean(participant.audio_muted),
1065
+ video_muted: Boolean(participant.video_muted)
1066
+ });
1067
+ if (!previous) this.onParticipantJoined(participant);
1068
+ if (participant.presence) this.onParticipantPresence(id, String(participant.presence));
1046
1069
  }
1047
1070
  const nextTracks = /* @__PURE__ */ new Map();
1048
- for (const track of payload.tracks || []) {
1049
- if (track?.track_id) nextTracks.set(String(track.track_id), track);
1071
+ for (const track of payload?.tracks || []) {
1072
+ if (!track?.track_id) continue;
1073
+ nextTracks.set(track.track_id, track);
1074
+ this.rememberTrack(track);
1050
1075
  }
1051
- for (const [trackId, existing] of this.trackMetadata) {
1052
- if (!nextTracks.has(trackId)) {
1053
- this.trackMetadata.delete(trackId);
1054
- this.trackToParticipant.delete(trackId);
1055
- this.trackToSource.delete(trackId);
1056
- this.subscribedTracks.delete(trackId);
1057
- for (const [source, localId] of this.localTrackIds) {
1058
- if (localId === trackId) this.localTrackIds.delete(source);
1059
- }
1060
- if (existing.participant_id !== this.currentParticipantId) {
1061
- this.onTrackUnpublished(existing);
1062
- }
1076
+ for (const id of [...this.trackMetadata.keys()]) {
1077
+ if (!nextTracks.has(id)) {
1078
+ this.trackMetadata.delete(id);
1079
+ this.subscribedTracks.delete(id);
1080
+ this.trackToParticipant.delete(id);
1081
+ this.trackToSource.delete(id);
1063
1082
  }
1064
1083
  }
1065
- for (const track of nextTracks.values()) {
1066
- this.rememberTrack(track);
1084
+ if (payload?.recording) {
1085
+ const current = {
1086
+ recording_id: payload.recording.recording_id,
1087
+ started_at: payload.recording.started_at
1088
+ };
1089
+ if (!this.activeRecording || this.activeRecording.recording_id !== current.recording_id) {
1090
+ this.activeRecording = current;
1091
+ this.onRecordingStarted(current);
1092
+ }
1093
+ } else if (this.activeRecording) {
1094
+ const previous = this.activeRecording;
1095
+ this.activeRecording = null;
1096
+ this.onRecordingStopped({
1097
+ recording_id: previous.recording_id,
1098
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString()
1099
+ });
1067
1100
  }
1068
1101
  if (this.autoSubscribe) {
1069
- const missing = [...nextTracks.values()].filter(
1070
- (track) => track.participant_id !== this.currentParticipantId && !this.subscribedTracks.has(track.track_id)
1071
- ).map((track) => track.track_id);
1072
- if (missing.length) {
1073
- await this.subscribeMultiple(missing);
1074
- }
1102
+ const missing = [...nextTracks.values()].filter((t) => t.participant_id !== this.currentParticipantId && !this.subscribedTracks.has(t.track_id)).map((t) => t.track_id);
1103
+ if (missing.length) await this.subscribeMultiple(missing);
1075
1104
  }
1076
1105
  this.onRoomState(payload);
1077
1106
  }
@@ -1105,9 +1134,17 @@ var SFUClient = class {
1105
1134
  if (track?.track_id) this.rememberTrack(track);
1106
1135
  }
1107
1136
  if (payload.recording) {
1108
- this.onRecordingStarted({
1137
+ this.activeRecording = {
1109
1138
  recording_id: payload.recording.recording_id,
1110
1139
  started_at: payload.recording.started_at
1140
+ };
1141
+ this.onRecordingStarted(this.activeRecording);
1142
+ } else if (this.activeRecording) {
1143
+ const previous = this.activeRecording;
1144
+ this.activeRecording = null;
1145
+ this.onRecordingStopped({
1146
+ recording_id: previous.recording_id,
1147
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString()
1111
1148
  });
1112
1149
  }
1113
1150
  await this.initializePeerConnections();
@@ -2035,9 +2072,13 @@ var VideoSDKCore = class {
2035
2072
  );
2036
2073
  return;
2037
2074
  }
2038
- throw new Error(
2039
- `SFU client already exists in state "${state}". Disconnect explicitly before creating another connection.`
2040
- );
2075
+ if (state === "disconnected" || state === "idle") {
2076
+ this.sfuClient = null;
2077
+ } else {
2078
+ throw new Error(
2079
+ `SFU client already exists in state "${state}". Disconnect explicitly before creating another connection.`
2080
+ );
2081
+ }
2041
2082
  }
2042
2083
  const client = new SFUClient({
2043
2084
  apiBase: this.apiBase,
@@ -2056,12 +2097,26 @@ var VideoSDKCore = class {
2056
2097
  });
2057
2098
  },
2058
2099
  onRoomState: (roomState) => {
2100
+ for (const participant of roomState?.participants || []) {
2101
+ const pid = participant?.participant_id;
2102
+ if (!pid || pid === this.myId) continue;
2103
+ const current = this.state.getParticipant(pid);
2104
+ if (!current) continue;
2105
+ this.state.updateParticipantMedia(pid, {
2106
+ stream: current.media?.stream ?? null,
2107
+ micEnabled: !Boolean(participant.audio_muted),
2108
+ camEnabled: !Boolean(participant.video_muted),
2109
+ isScreenSharing: Boolean(participant.screen_sharing)
2110
+ });
2111
+ }
2112
+ const presenters = /* @__PURE__ */ new Set();
2059
2113
  for (const participant of roomState?.participants || []) {
2060
2114
  const pid = participant?.participant_id;
2061
2115
  if (!pid || pid === this.myId) continue;
2062
2116
  const current = this.state.getParticipant(pid);
2063
2117
  if (!current) continue;
2064
2118
  if (participant.screen_sharing) {
2119
+ presenters.add(pid);
2065
2120
  this.state.updateParticipantMedia(pid, {
2066
2121
  isScreenSharing: true,
2067
2122
  screenStream: current.media?.screenStream ?? null
@@ -2077,6 +2132,10 @@ var VideoSDKCore = class {
2077
2132
  });
2078
2133
  }
2079
2134
  }
2135
+ const presenter = [...presenters][0] ?? null;
2136
+ if (this.state.presenterId !== presenter) {
2137
+ this.state.setPresenterId(presenter);
2138
+ }
2080
2139
  this.events.onRoomUpdate?.(roomState);
2081
2140
  },
2082
2141
  onDisconnected: () => {
@@ -2150,8 +2209,8 @@ var VideoSDKCore = class {
2150
2209
  name: pname,
2151
2210
  connectionState: "connected",
2152
2211
  media: {
2153
- micEnabled: true,
2154
- camEnabled: true,
2212
+ micEnabled: !Boolean(participant.audio_muted),
2213
+ camEnabled: !Boolean(participant.video_muted),
2155
2214
  isScreenSharing: false,
2156
2215
  stream: null
2157
2216
  }
@@ -2224,6 +2283,18 @@ var VideoSDKCore = class {
2224
2283
  camEnabled: track.kind === "video" ? false : current.media?.camEnabled ?? true
2225
2284
  });
2226
2285
  },
2286
+ onParticipantMediaState: (participantId, audioMuted, videoMuted) => {
2287
+ if (participantId === this.myId) return;
2288
+ const current = this.state.getParticipant(participantId);
2289
+ if (!current) return;
2290
+ this.state.updateParticipantMedia(participantId, {
2291
+ stream: current.media?.stream ?? null,
2292
+ micEnabled: !audioMuted,
2293
+ camEnabled: !videoMuted
2294
+ });
2295
+ this.events.onMuteStateChanged?.(participantId, "audio", audioMuted);
2296
+ this.events.onMuteStateChanged?.(participantId, "video", videoMuted);
2297
+ },
2227
2298
  onTrackStateChanged: (track) => {
2228
2299
  const pid = track.participant_id;
2229
2300
  if (!pid || pid === this.myId) {
@@ -2324,7 +2395,9 @@ var VideoSDKCore = class {
2324
2395
  client.setLocalStream(this.localStream);
2325
2396
  await client.getToken(roomId, {
2326
2397
  displayName: name,
2327
- participantId: this.myId
2398
+ participantId: this.myId,
2399
+ audioMuted: this.state.localParticipant?.media?.micEnabled,
2400
+ videoMuted: this.state.localParticipant?.media?.camEnabled
2328
2401
  });
2329
2402
  }
2330
2403
  // SFU STATE