@defcomm/meetingsdk 0.2.3 → 0.2.5

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
@@ -392,8 +392,7 @@ var SFUClient = class {
392
392
  this.trackToSource = /* @__PURE__ */ new Map();
393
393
  this.localTrackIds = /* @__PURE__ */ new Map();
394
394
  this.subscriberStatsTimer = null;
395
- this.roomSyncTimer = null;
396
- this.roomSyncInFlight = false;
395
+ this.lastRecordingId = null;
397
396
  this.heartbeatTimer = null;
398
397
  this.apiBase = options.apiBase || "https://api.example.com/api";
399
398
  this.wsBase = options.wsBase || "wss://api.example.com/ws";
@@ -406,6 +405,8 @@ var SFUClient = class {
406
405
  });
407
406
  this.onParticipantPresence = options.onParticipantPresence || (() => {
408
407
  });
408
+ this.onParticipantMediaState = options.onParticipantMediaState || (() => {
409
+ });
409
410
  this.onTrackPublished = options.onTrackPublished || (() => {
410
411
  });
411
412
  this.onTrackUnpublished = options.onTrackUnpublished || (() => {
@@ -436,8 +437,6 @@ var SFUClient = class {
436
437
  });
437
438
  this.onRecordingStopped = options.onRecordingStopped || (() => {
438
439
  });
439
- this.onRoomState = options.onRoomState || (() => {
440
- });
441
440
  this.onError = options.onError || console.error;
442
441
  this.autoSubscribe = options.autoSubscribe !== false;
443
442
  }
@@ -459,29 +458,6 @@ var SFUClient = class {
459
458
  this.heartbeatTimer = null;
460
459
  }
461
460
  }
462
- startRoomStateSync() {
463
- this.stopRoomStateSync();
464
- this.roomSyncTimer = setInterval(() => {
465
- if (this.state !== "connected") return;
466
- void this.syncRoomState();
467
- }, 5e3);
468
- }
469
- stopRoomStateSync() {
470
- if (this.roomSyncTimer) {
471
- clearInterval(this.roomSyncTimer);
472
- this.roomSyncTimer = null;
473
- }
474
- this.roomSyncInFlight = false;
475
- }
476
- async syncRoomState() {
477
- if (this.roomSyncInFlight || !this.isSocketOpen()) return;
478
- this.roomSyncInFlight = true;
479
- try {
480
- this.send({ type: "sync_room_state" });
481
- } finally {
482
- this.roomSyncInFlight = false;
483
- }
484
- }
485
461
  createId() {
486
462
  return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `id_${Math.random().toString(36).slice(2)}_${Date.now()}`;
487
463
  }
@@ -581,7 +557,9 @@ var SFUClient = class {
581
557
  this.room = nextRoom;
582
558
  this.joinOptions = {
583
559
  displayName: options.displayName,
584
- avatarUrl: options.avatarUrl
560
+ avatarUrl: options.avatarUrl,
561
+ audioMuted: options.audioMuted,
562
+ videoMuted: options.videoMuted
585
563
  };
586
564
  if (options.participantId) this.participantId = options.participantId;
587
565
  if (!this.resumeToken) {
@@ -650,7 +628,9 @@ var SFUClient = class {
650
628
  room_code: this.room,
651
629
  resume_token: this.resumeToken,
652
630
  display_name: options.displayName,
653
- avatar_url: options.avatarUrl
631
+ avatar_url: options.avatarUrl,
632
+ audio_muted: options.audioMuted ?? this.joinOptions.audioMuted ?? false,
633
+ video_muted: options.videoMuted ?? this.joinOptions.videoMuted ?? false
654
634
  };
655
635
  if (this.pendingJoinRequestId) {
656
636
  this.send({
@@ -764,7 +744,9 @@ var SFUClient = class {
764
744
  room_code: this.room,
765
745
  resume_token: this.resumeToken,
766
746
  display_name: this.joinOptions.displayName,
767
- avatar_url: this.joinOptions.avatarUrl
747
+ avatar_url: this.joinOptions.avatarUrl,
748
+ audio_muted: this.joinOptions.audioMuted ?? false,
749
+ video_muted: this.joinOptions.videoMuted ?? false
768
750
  }
769
751
  });
770
752
  }
@@ -904,11 +886,8 @@ var SFUClient = class {
904
886
  case "subscription_updated": {
905
887
  break;
906
888
  }
907
- case "room_state": {
908
- await this.applyRoomState(payload);
909
- break;
910
- }
911
889
  case "recording_started": {
890
+ this.lastRecordingId = payload.recording_id || this.lastRecordingId;
912
891
  this.onRecordingStarted({
913
892
  recording_id: payload.recording_id,
914
893
  started_at: payload.started_at
@@ -916,6 +895,7 @@ var SFUClient = class {
916
895
  break;
917
896
  }
918
897
  case "recording_stopped": {
898
+ this.lastRecordingId = null;
919
899
  this.onRecordingStopped({
920
900
  recording_id: payload.recording_id,
921
901
  stopped_at: payload.stopped_at
@@ -951,6 +931,29 @@ var SFUClient = class {
951
931
  this.onScreenShareStopped(participantId, trackId);
952
932
  break;
953
933
  }
934
+ case "room_state": {
935
+ await this.reconcileRoomState(payload);
936
+ break;
937
+ }
938
+ case "participant_media_state": {
939
+ const participantId = payload.participant_id;
940
+ if (participantId) {
941
+ const current = this.participants.get(participantId);
942
+ if (current) {
943
+ this.participants.set(participantId, {
944
+ ...current,
945
+ audio_muted: Boolean(payload.audio_muted),
946
+ video_muted: Boolean(payload.video_muted)
947
+ });
948
+ }
949
+ }
950
+ this.onParticipantMediaState?.(
951
+ participantId,
952
+ Boolean(payload.audio_muted),
953
+ Boolean(payload.video_muted)
954
+ );
955
+ break;
956
+ }
954
957
  case "error": {
955
958
  const message2 = payload.message || "SFU signaling error";
956
959
  if (this.state === "reconnecting" && message2.includes("participant session is stale")) {
@@ -973,71 +976,89 @@ var SFUClient = class {
973
976
  console.debug("[SFU] Ignoring unknown server message", message);
974
977
  }
975
978
  }
976
- async applyRoomState(payload) {
979
+ async reconcileRoomState(payload) {
977
980
  const nextParticipants = /* @__PURE__ */ new Map();
978
981
  for (const participant of payload.participants || []) {
979
- const id = participant?.participant_id;
980
- if (!id) continue;
981
- nextParticipants.set(id, participant);
982
- }
983
- for (const [id] of this.participants) {
984
- if (id === this.currentParticipantId) continue;
985
- if (!nextParticipants.has(id)) {
986
- this.participants.delete(id);
987
- this.removeRemoteParticipant(id);
988
- this.onParticipantLeft(id);
989
- }
990
- }
991
- for (const [id, participant] of nextParticipants) {
992
- if (id === this.currentParticipantId) continue;
993
- const existed = this.participants.has(id);
994
- this.participants.set(id, participant);
995
- if (!existed) {
982
+ if (participant?.participant_id) {
983
+ nextParticipants.set(participant.participant_id, participant);
984
+ }
985
+ }
986
+ for (const participantId of this.participants.keys()) {
987
+ if (participantId !== this.currentParticipantId && !nextParticipants.has(participantId)) {
988
+ this.participants.delete(participantId);
989
+ this.onParticipantLeft(participantId);
990
+ }
991
+ }
992
+ for (const participant of nextParticipants.values()) {
993
+ if (participant.participant_id === this.currentParticipantId) continue;
994
+ const previous = this.participants.get(participant.participant_id);
995
+ const wasScreenSharing = Boolean(previous?.screen_sharing);
996
+ const isScreenSharing = Boolean(participant.screen_sharing);
997
+ this.participants.set(participant.participant_id, participant);
998
+ if (!previous) {
996
999
  this.onParticipantJoined(participant);
997
1000
  }
998
- const sharing = Boolean(participant.screen_sharing);
999
- const screenTrackId = participant.screen_track_id ? String(participant.screen_track_id) : void 0;
1000
- if (sharing) {
1001
- const track = screenTrackId ? (payload.tracks || []).find(
1002
- (candidate) => String(candidate.track_id) === screenTrackId
1003
- ) : void 0;
1004
- this.onScreenShareStarted(id, track);
1005
- } else if (this.remoteScreenStreams.has(id)) {
1006
- this.remoteScreenStreams.get(id)?.getTracks().forEach((track) => track.stop());
1007
- this.remoteScreenStreams.delete(id);
1008
- this.onScreenShareStopped(id, screenTrackId);
1001
+ if (participant.presence) {
1002
+ this.onParticipantPresence(
1003
+ participant.participant_id,
1004
+ String(participant.presence).toLowerCase()
1005
+ );
1006
+ }
1007
+ this.onParticipantMediaState(
1008
+ participant.participant_id,
1009
+ Boolean(participant.audio_muted),
1010
+ Boolean(participant.video_muted)
1011
+ );
1012
+ if (!wasScreenSharing && isScreenSharing) {
1013
+ this.onScreenShareStarted(participant.participant_id, void 0);
1014
+ } else if (wasScreenSharing && !isScreenSharing) {
1015
+ this.onScreenShareStopped(
1016
+ participant.participant_id,
1017
+ participant.screen_track_id || void 0
1018
+ );
1009
1019
  }
1010
1020
  }
1011
1021
  const nextTracks = /* @__PURE__ */ new Map();
1012
1022
  for (const track of payload.tracks || []) {
1013
- if (track?.track_id) nextTracks.set(String(track.track_id), track);
1023
+ if (track?.track_id) {
1024
+ nextTracks.set(track.track_id, track);
1025
+ this.rememberTrack(track);
1026
+ }
1014
1027
  }
1015
- for (const [trackId, existing] of this.trackMetadata) {
1028
+ for (const [trackId, oldTrack] of [...this.trackMetadata.entries()]) {
1016
1029
  if (!nextTracks.has(trackId)) {
1017
1030
  this.trackMetadata.delete(trackId);
1018
1031
  this.trackToParticipant.delete(trackId);
1019
1032
  this.trackToSource.delete(trackId);
1020
1033
  this.subscribedTracks.delete(trackId);
1021
- for (const [source, localId] of this.localTrackIds) {
1022
- if (localId === trackId) this.localTrackIds.delete(source);
1023
- }
1024
- if (existing.participant_id !== this.currentParticipantId) {
1025
- this.onTrackUnpublished(existing);
1034
+ if (oldTrack.participant_id !== this.currentParticipantId) {
1035
+ this.onTrackUnpublished(oldTrack);
1026
1036
  }
1027
1037
  }
1028
1038
  }
1029
- for (const track of nextTracks.values()) {
1030
- this.rememberTrack(track);
1031
- }
1032
1039
  if (this.autoSubscribe) {
1033
1040
  const missing = [...nextTracks.values()].filter(
1034
1041
  (track) => track.participant_id !== this.currentParticipantId && !this.subscribedTracks.has(track.track_id)
1035
1042
  ).map((track) => track.track_id);
1036
- if (missing.length) {
1037
- await this.subscribeMultiple(missing);
1043
+ if (missing.length) await this.subscribeMultiple(missing);
1044
+ }
1045
+ const nextRecordingId = payload.recording?.recording_id || null;
1046
+ if (payload.recording) {
1047
+ if (this.lastRecordingId !== nextRecordingId) {
1048
+ this.lastRecordingId = nextRecordingId;
1049
+ this.onRecordingStarted({
1050
+ recording_id: payload.recording.recording_id,
1051
+ started_at: payload.recording.started_at
1052
+ });
1038
1053
  }
1054
+ } else if (this.lastRecordingId) {
1055
+ const recordingId = this.lastRecordingId;
1056
+ this.lastRecordingId = null;
1057
+ this.onRecordingStopped({
1058
+ recording_id: recordingId,
1059
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString()
1060
+ });
1039
1061
  }
1040
- this.onRoomState(payload);
1041
1062
  }
1042
1063
  rememberTrack(track) {
1043
1064
  this.trackMetadata.set(track.track_id, track);
@@ -1068,15 +1089,8 @@ var SFUClient = class {
1068
1089
  for (const track of payload.tracks || []) {
1069
1090
  if (track?.track_id) this.rememberTrack(track);
1070
1091
  }
1071
- if (payload.recording) {
1072
- this.onRecordingStarted({
1073
- recording_id: payload.recording.recording_id,
1074
- started_at: payload.recording.started_at
1075
- });
1076
- }
1077
1092
  await this.initializePeerConnections();
1078
1093
  this.setState("connected");
1079
- this.startRoomStateSync();
1080
1094
  this.onConnected(payload, resumed);
1081
1095
  for (const participant of this.participants.values()) {
1082
1096
  if (participant.participant_id !== this.currentParticipantId) {
@@ -1532,31 +1546,25 @@ var SFUClient = class {
1532
1546
  this.localStream?.getAudioTracks().forEach((track) => {
1533
1547
  track.enabled = !muted;
1534
1548
  });
1535
- const trackId = this.localTrackIds.get("audio");
1536
- if (trackId) {
1537
- this.send({
1538
- type: "track_state",
1539
- payload: {
1540
- track_id: trackId,
1541
- muted
1542
- }
1543
- });
1544
- }
1549
+ this.send({
1550
+ type: "set_media_state",
1551
+ payload: {
1552
+ audio_muted: muted,
1553
+ video_muted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? true)
1554
+ }
1555
+ });
1545
1556
  }
1546
1557
  muteVideo(muted) {
1547
1558
  this.localStream?.getVideoTracks().forEach((track) => {
1548
1559
  track.enabled = !muted;
1549
1560
  });
1550
- const trackId = this.localTrackIds.get("camera");
1551
- if (trackId) {
1552
- this.send({
1553
- type: "track_state",
1554
- payload: {
1555
- track_id: trackId,
1556
- muted
1557
- }
1558
- });
1559
- }
1561
+ this.send({
1562
+ type: "set_media_state",
1563
+ payload: {
1564
+ audio_muted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? true),
1565
+ video_muted: muted
1566
+ }
1567
+ });
1560
1568
  }
1561
1569
  async startRecording() {
1562
1570
  if (!this.send({ type: "start_recording" })) {
@@ -1631,7 +1639,6 @@ var SFUClient = class {
1631
1639
  }
1632
1640
  async leave() {
1633
1641
  this.stopHeartbeat();
1634
- this.stopRoomStateSync();
1635
1642
  this.intentionalClose = true;
1636
1643
  this.setState("leaving");
1637
1644
  if (this.reconnectTimer) {
@@ -2016,32 +2023,12 @@ var VideoSDKCore = class {
2016
2023
  console.log(`[SFU] ${resumed ? "Resumed" : "Joined"} room`, payload);
2017
2024
  this.state.updateLocalParticipant({
2018
2025
  id: this.myId,
2019
- name: this.participantName
2020
- });
2021
- },
2022
- onRoomState: (roomState) => {
2023
- for (const participant of roomState?.participants || []) {
2024
- const pid = participant?.participant_id;
2025
- if (!pid || pid === this.myId) continue;
2026
- const current = this.state.getParticipant(pid);
2027
- if (!current) continue;
2028
- if (participant.screen_sharing) {
2029
- this.state.updateParticipantMedia(pid, {
2030
- isScreenSharing: true,
2031
- screenStream: current.media?.screenStream ?? null
2032
- });
2033
- } else if (current.media?.isScreenSharing) {
2034
- const screen = this.remoteScreenStreams.get(pid);
2035
- screen?.getTracks().forEach((track) => track.stop());
2036
- this.remoteScreenStreams.delete(pid);
2037
- this.state.updateParticipantMedia(pid, {
2038
- isScreenSharing: false,
2039
- screenStream: null,
2040
- screenTrack: void 0
2041
- });
2026
+ name: this.participantName,
2027
+ media: {
2028
+ micEnabled: this.localStream?.getAudioTracks()[0]?.enabled ?? true,
2029
+ camEnabled: this.localStream?.getVideoTracks()[0]?.enabled ?? true
2042
2030
  }
2043
- }
2044
- this.events.onRoomUpdate?.(roomState);
2031
+ });
2045
2032
  },
2046
2033
  onDisconnected: () => {
2047
2034
  console.log("[SFU] Signaling WebSocket disconnected; reconnecting");
@@ -2109,22 +2096,44 @@ var VideoSDKCore = class {
2109
2096
  return;
2110
2097
  }
2111
2098
  const pname = participant.user_metadata?.name || participant.display_name || "Participant";
2112
- this.state.addParticipant({
2099
+ const participantMedia = participant.media || {};
2100
+ const participantState = {
2113
2101
  id: pid,
2114
2102
  name: pname,
2115
- connectionState: "connected",
2103
+ connectionState: participant.presence === "reconnecting" ? "reconnecting" : "connected",
2116
2104
  media: {
2117
- micEnabled: true,
2118
- camEnabled: true,
2119
- isScreenSharing: false,
2105
+ micEnabled: !Boolean(participant.audio_muted),
2106
+ camEnabled: !Boolean(participant.video_muted),
2107
+ isScreenSharing: Boolean(participant.screen_sharing),
2108
+ screenStream: participantMedia.screenStream ?? null,
2120
2109
  stream: null
2121
2110
  }
2122
- });
2111
+ };
2112
+ if (!this.state.addParticipant(participantState)) {
2113
+ this.state.updateParticipantMedia(pid, participantState.media);
2114
+ }
2123
2115
  this.events.onUserJoined?.({
2124
2116
  id: pid,
2125
2117
  name: pname
2126
2118
  });
2127
2119
  },
2120
+ onParticipantMediaState: (participantId, audioMuted, videoMuted) => {
2121
+ if (participantId === this.myId) {
2122
+ this.state.updateLocalParticipant({
2123
+ media: {
2124
+ micEnabled: !audioMuted,
2125
+ camEnabled: !videoMuted
2126
+ }
2127
+ });
2128
+ return;
2129
+ }
2130
+ this.state.updateParticipantMedia(participantId, {
2131
+ micEnabled: !audioMuted,
2132
+ camEnabled: !videoMuted
2133
+ });
2134
+ this.events.onMuteStateChanged?.(participantId, "audio", audioMuted);
2135
+ this.events.onMuteStateChanged?.(participantId, "video", videoMuted);
2136
+ },
2128
2137
  onParticipantPresence: (participantId, presence) => {
2129
2138
  if (participantId === this.myId) {
2130
2139
  return;
@@ -2288,7 +2297,9 @@ var VideoSDKCore = class {
2288
2297
  client.setLocalStream(this.localStream);
2289
2298
  await client.getToken(roomId, {
2290
2299
  displayName: name,
2291
- participantId: this.myId
2300
+ participantId: this.myId,
2301
+ audioMuted: !this.localStream?.getAudioTracks()[0]?.enabled,
2302
+ videoMuted: !this.localStream?.getVideoTracks()[0]?.enabled
2292
2303
  });
2293
2304
  }
2294
2305
  // SFU STATE
@@ -2452,6 +2463,7 @@ var VideoSDKCore = class {
2452
2463
  screenTrack: this.screenStream.getVideoTracks()[0]
2453
2464
  }
2454
2465
  });
2466
+ this.state.setPresenterId(this.myId);
2455
2467
  return this.screenStream;
2456
2468
  } catch (err) {
2457
2469
  this.emitError(
@@ -2473,6 +2485,9 @@ var VideoSDKCore = class {
2473
2485
  this.screenStream.getTracks().forEach((track) => track.stop());
2474
2486
  this.screenStream = null;
2475
2487
  this.isScreenSharing = false;
2488
+ if (this.state.presenterId === this.myId) {
2489
+ this.state.setPresenterId(null);
2490
+ }
2476
2491
  this.state.updateLocalParticipant({
2477
2492
  media: {
2478
2493
  isScreenSharing: false,
@@ -2602,7 +2617,6 @@ var MeetingProvider = ({
2602
2617
  /* @__PURE__ */ new Set()
2603
2618
  );
2604
2619
  const meetingLeftListeners = useRef(/* @__PURE__ */ new Set());
2605
- const roomUpdateListeners = useRef(/* @__PURE__ */ new Set());
2606
2620
  const trackStateListeners = useRef(/* @__PURE__ */ new Set());
2607
2621
  const recordingStartedListeners = useRef(/* @__PURE__ */ new Set());
2608
2622
  const recordingStoppedListeners = useRef(/* @__PURE__ */ new Set());
@@ -2627,9 +2641,6 @@ var MeetingProvider = ({
2627
2641
  onMeetingLeft: () => {
2628
2642
  meetingLeftListeners.current.forEach((fn) => fn());
2629
2643
  },
2630
- onRoomUpdate: (data) => {
2631
- roomUpdateListeners.current.forEach((fn) => fn(data));
2632
- },
2633
2644
  onTrackStateChanged: (track) => {
2634
2645
  trackStateListeners.current.forEach((fn) => fn(track));
2635
2646
  },
@@ -2728,10 +2739,6 @@ var MeetingProvider = ({
2728
2739
  meetingLeftListeners.current.delete(cb);
2729
2740
  };
2730
2741
  },
2731
- onRoomUpdate: (cb) => {
2732
- roomUpdateListeners.current.add(cb);
2733
- return () => roomUpdateListeners.current.delete(cb);
2734
- },
2735
2742
  onTrackStateChanged: (cb) => {
2736
2743
  trackStateListeners.current.add(cb);
2737
2744
  return () => trackStateListeners.current.delete(cb);