@defcomm/meetingsdk 0.2.6 → 0.2.8

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.js CHANGED
@@ -393,6 +393,7 @@ var SFUClient = class {
393
393
  this.iceServers = [];
394
394
  this.requestId = 0;
395
395
  this.publisherRevision = 0;
396
+ this.screenShareGeneration = 0;
396
397
  this.currentParticipantId = null;
397
398
  this.token = null;
398
399
  this.room = null;
@@ -403,6 +404,8 @@ var SFUClient = class {
403
404
  this.intentionalClose = false;
404
405
  this.leaveAcknowledged = false;
405
406
  this.joiningWithResume = false;
407
+ this.desiredAudioMuted = false;
408
+ this.desiredVideoMuted = false;
406
409
  this.reconnectTimer = null;
407
410
  this.reconnectAttempt = 0;
408
411
  this.maxReconnectAttempts = 8;
@@ -471,6 +474,14 @@ var SFUClient = class {
471
474
  this.onRecordingStopped = options.onRecordingStopped || (() => {
472
475
  });
473
476
  this.onError = options.onError || console.error;
477
+ this.onChatMessage = options.onChatMessage || (() => {
478
+ });
479
+ this.onReaction = options.onReaction || (() => {
480
+ });
481
+ this.onRoomUpdate = options.onRoomUpdate || (() => {
482
+ });
483
+ this.onParticipantMediaState = options.onParticipantMediaState || (() => {
484
+ });
474
485
  this.autoSubscribe = options.autoSubscribe !== false;
475
486
  }
476
487
  startHeartbeat() {
@@ -593,6 +604,10 @@ var SFUClient = class {
593
604
  avatarUrl: options.avatarUrl
594
605
  };
595
606
  if (options.participantId) this.participantId = options.participantId;
607
+ if (typeof options.audioMuted === "boolean")
608
+ this.desiredAudioMuted = options.audioMuted;
609
+ if (typeof options.videoMuted === "boolean")
610
+ this.desiredVideoMuted = options.videoMuted;
596
611
  if (!this.resumeToken) {
597
612
  this.resumeToken = this.loadResumeToken(this.room);
598
613
  }
@@ -659,7 +674,9 @@ var SFUClient = class {
659
674
  room_code: this.room,
660
675
  resume_token: this.resumeToken,
661
676
  display_name: options.displayName,
662
- avatar_url: options.avatarUrl
677
+ avatar_url: options.avatarUrl,
678
+ audio_muted: this.desiredAudioMuted,
679
+ video_muted: this.desiredVideoMuted
663
680
  };
664
681
  if (this.pendingJoinRequestId) {
665
682
  this.send({
@@ -834,6 +851,16 @@ var SFUClient = class {
834
851
  this.onParticipantLeft(participantId);
835
852
  break;
836
853
  }
854
+ case "participant_media_state": {
855
+ const participantId = payload.participant_id;
856
+ if (!participantId) break;
857
+ this.onParticipantMediaState(
858
+ participantId,
859
+ Boolean(payload.audio_muted),
860
+ Boolean(payload.video_muted)
861
+ );
862
+ break;
863
+ }
837
864
  case "participant_presence":
838
865
  case "participant_presence_changed": {
839
866
  const participantId = payload.participant_id;
@@ -956,6 +983,50 @@ var SFUClient = class {
956
983
  this.onScreenShareStopped(participantId, trackId);
957
984
  break;
958
985
  }
986
+ case "room_state": {
987
+ const participants = payload.participants || [];
988
+ this.onRoomStateReconciled(payload);
989
+ this.onRoomUpdate(payload);
990
+ for (const participant of participants) {
991
+ const pid = participant?.participant_id;
992
+ if (!pid) continue;
993
+ if (pid !== this.currentParticipantId) {
994
+ this.participants.set(pid, participant);
995
+ this.onParticipantPresence(
996
+ pid,
997
+ String(participant.presence || "connected")
998
+ );
999
+ this.onParticipantMediaState(
1000
+ pid,
1001
+ Boolean(participant.audio_muted),
1002
+ Boolean(participant.video_muted)
1003
+ );
1004
+ }
1005
+ }
1006
+ for (const [pid] of this.participants) {
1007
+ if (!participants.some((p) => p?.participant_id === pid)) {
1008
+ this.participants.delete(pid);
1009
+ this.removeRemoteParticipant(pid);
1010
+ this.onParticipantLeft(pid);
1011
+ }
1012
+ }
1013
+ for (const track of payload.tracks || []) this.rememberTrack(track);
1014
+ if (Array.isArray(payload.chat_history)) {
1015
+ for (const message2 of payload.chat_history)
1016
+ this.onChatMessage(message2);
1017
+ }
1018
+ break;
1019
+ }
1020
+ case "chat_message": {
1021
+ const msg = payload.message || payload;
1022
+ if (msg?.id) this.onChatMessage(msg);
1023
+ break;
1024
+ }
1025
+ case "reaction": {
1026
+ const reaction = payload.reaction || payload;
1027
+ if (reaction?.id && reaction?.emoji) this.onReaction(reaction);
1028
+ break;
1029
+ }
959
1030
  case "error": {
960
1031
  const message2 = payload.message || "SFU signaling error";
961
1032
  if (this.state === "reconnecting" && message2.includes("participant session is stale")) {
@@ -978,6 +1049,16 @@ var SFUClient = class {
978
1049
  console.debug("[SFU] Ignoring unknown server message", message);
979
1050
  }
980
1051
  }
1052
+ onRoomStateReconciled(payload) {
1053
+ const participants = payload.participants || [];
1054
+ const local = participants.find(
1055
+ (p) => p?.participant_id === this.currentParticipantId
1056
+ );
1057
+ if (local) {
1058
+ this.desiredAudioMuted = Boolean(local.audio_muted);
1059
+ this.desiredVideoMuted = Boolean(local.video_muted);
1060
+ }
1061
+ }
981
1062
  rememberTrack(track) {
982
1063
  this.trackMetadata.set(track.track_id, track);
983
1064
  this.trackToParticipant.set(track.track_id, track.participant_id);
@@ -1013,8 +1094,21 @@ var SFUClient = class {
1013
1094
  for (const participant of this.participants.values()) {
1014
1095
  if (participant.participant_id !== this.currentParticipantId) {
1015
1096
  this.onParticipantJoined(participant);
1097
+ this.onParticipantPresence(
1098
+ participant.participant_id,
1099
+ String(participant.presence || "connected")
1100
+ );
1101
+ this.onParticipantMediaState(
1102
+ participant.participant_id,
1103
+ Boolean(participant.audio_muted),
1104
+ Boolean(participant.video_muted)
1105
+ );
1016
1106
  }
1017
1107
  }
1108
+ if (Array.isArray(payload.chat_history)) {
1109
+ for (const message of payload.chat_history) this.onChatMessage(message);
1110
+ }
1111
+ this.onRoomUpdate(payload);
1018
1112
  if (this.localStream) {
1019
1113
  await this.publishExistingTracks();
1020
1114
  }
@@ -1373,9 +1467,27 @@ var SFUClient = class {
1373
1467
  if (!this.screenTransceiver || !this.screenSender) {
1374
1468
  throw new Error("Screen transceiver is not initialized");
1375
1469
  }
1376
- if (this.screenStream) {
1470
+ const existingTrack = this.screenSender.track;
1471
+ if (this.screenStream && existingTrack && existingTrack.readyState === "live") {
1377
1472
  return this.screenStream;
1378
1473
  }
1474
+ const generation = ++this.screenShareGeneration;
1475
+ try {
1476
+ await this.screenSender.replaceTrack(null);
1477
+ } catch (err) {
1478
+ console.warn("[SFU] Failed to clear previous screen track", err);
1479
+ }
1480
+ if (this.screenStream) {
1481
+ for (const track of this.screenStream.getTracks()) {
1482
+ try {
1483
+ track.onended = null;
1484
+ track.stop();
1485
+ } catch {
1486
+ }
1487
+ }
1488
+ }
1489
+ this.screenStream = null;
1490
+ this.screenTransceiver.direction = "inactive";
1379
1491
  const screenStream = await navigator.mediaDevices.getDisplayMedia({
1380
1492
  video: true,
1381
1493
  audio: false
@@ -1387,83 +1499,99 @@ var SFUClient = class {
1387
1499
  }
1388
1500
  this.screenStream = screenStream;
1389
1501
  try {
1390
- if (!this.send({ type: "start_screen_share" })) {
1391
- throw new Error("Failed to signal start_screen_share");
1392
- }
1393
1502
  await this.screenSender.replaceTrack(screenTrack);
1394
1503
  this.screenTransceiver.direction = "sendonly";
1504
+ this.send({
1505
+ type: "start_screen_share"
1506
+ });
1507
+ await new Promise((resolve) => {
1508
+ queueMicrotask(resolve);
1509
+ });
1395
1510
  const offer = await this.publisher.createOffer();
1396
1511
  await this.publisher.setLocalDescription(offer);
1512
+ const localDescription = this.publisher.localDescription;
1513
+ if (!localDescription?.sdp) {
1514
+ throw new Error("Publisher did not produce a valid screen-share SDP");
1515
+ }
1516
+ const revision = ++this.publisherRevision;
1397
1517
  this.send({
1398
1518
  type: "publish_offer",
1399
1519
  payload: {
1400
- revision: ++this.publisherRevision,
1401
- sdp: offer.sdp
1520
+ revision,
1521
+ sdp: localDescription.sdp
1402
1522
  }
1403
1523
  });
1404
1524
  screenTrack.onended = () => {
1525
+ if (generation !== this.screenShareGeneration) {
1526
+ return;
1527
+ }
1405
1528
  void this.stopScreenShare();
1406
1529
  };
1407
- console.info("[publisher] Screen track attached", {
1408
- screenTrackId: screenTrack.id,
1409
- cameraTrackId: this.videoSender?.track?.id,
1410
- screenMid: this.screenTransceiver.mid,
1411
- cameraMid: this.cameraTransceiver?.mid
1412
- });
1413
- return this.screenStream;
1530
+ return screenStream;
1414
1531
  } catch (error) {
1415
- try {
1416
- await this.screenSender.replaceTrack(null);
1417
- } catch {
1418
- }
1419
- this.screenTransceiver.direction = "inactive";
1420
- screenStream.getTracks().forEach((track) => track.stop());
1421
- this.screenStream = null;
1422
- try {
1423
- this.send({ type: "stop_screen_share" });
1424
- } catch {
1532
+ if (generation === this.screenShareGeneration) {
1533
+ try {
1534
+ await this.screenSender.replaceTrack(null);
1535
+ } catch {
1536
+ }
1537
+ this.screenTransceiver.direction = "inactive";
1538
+ screenTrack.onended = null;
1539
+ screenStream.getTracks().forEach((track) => {
1540
+ try {
1541
+ track.stop();
1542
+ } catch {
1543
+ }
1544
+ });
1545
+ this.screenStream = null;
1546
+ try {
1547
+ this.send({
1548
+ type: "stop_screen_share"
1549
+ });
1550
+ } catch {
1551
+ }
1425
1552
  }
1426
1553
  throw error;
1427
1554
  }
1428
1555
  }
1429
1556
  async stopScreenShare() {
1430
- if (!this.screenStream && !this.screenSender?.track) return;
1431
- const screenTrack = this.screenSender?.track || null;
1432
- if (screenTrack) screenTrack.onended = null;
1557
+ const stream = this.screenStream;
1558
+ const senderTrack = this.screenSender?.track || null;
1559
+ if (!stream && !senderTrack) return;
1560
+ ++this.screenShareGeneration;
1561
+ if (senderTrack) senderTrack.onended = null;
1433
1562
  try {
1434
1563
  await this.screenSender?.replaceTrack(null);
1435
1564
  } catch (error) {
1436
1565
  this.onError(error);
1437
1566
  }
1438
- if (this.screenTransceiver) {
1439
- this.screenTransceiver.direction = "inactive";
1440
- }
1441
- this.send({ type: "stop_screen_share" });
1567
+ if (this.screenTransceiver) this.screenTransceiver.direction = "inactive";
1442
1568
  try {
1443
1569
  const offer = await this.publisher?.createOffer();
1444
1570
  if (offer && this.publisher) {
1445
1571
  await this.publisher.setLocalDescription(offer);
1446
1572
  this.send({
1447
1573
  type: "publish_offer",
1448
- payload: {
1449
- revision: ++this.publisherRevision,
1450
- sdp: offer.sdp
1451
- }
1574
+ payload: { revision: ++this.publisherRevision, sdp: offer.sdp }
1452
1575
  });
1453
1576
  }
1454
1577
  } catch (error) {
1455
1578
  this.onError(error);
1456
1579
  }
1457
- this.screenStream?.getTracks().forEach((track) => track.stop());
1580
+ this.send({ type: "stop_screen_share" });
1581
+ stream?.getTracks().forEach((track) => track.stop());
1458
1582
  this.screenStream = null;
1459
- if (this.currentParticipantId) {
1583
+ if (this.currentParticipantId)
1460
1584
  this.onScreenShareStopped(this.currentParticipantId);
1461
- }
1462
1585
  }
1463
1586
  muteAudio(muted) {
1587
+ this.desiredAudioMuted = muted;
1464
1588
  this.localStream?.getAudioTracks().forEach((track) => {
1465
1589
  track.enabled = !muted;
1466
1590
  });
1591
+ this.send({
1592
+ type: "set_media_state",
1593
+ payload: { audio_muted: muted, video_muted: this.desiredVideoMuted }
1594
+ });
1467
1595
  const trackId = this.localTrackIds.get("audio");
1468
1596
  if (trackId) {
1469
1597
  this.send({
@@ -1476,9 +1604,14 @@ var SFUClient = class {
1476
1604
  }
1477
1605
  }
1478
1606
  muteVideo(muted) {
1607
+ this.desiredVideoMuted = muted;
1479
1608
  this.localStream?.getVideoTracks().forEach((track) => {
1480
1609
  track.enabled = !muted;
1481
1610
  });
1611
+ this.send({
1612
+ type: "set_media_state",
1613
+ payload: { audio_muted: this.desiredAudioMuted, video_muted: muted }
1614
+ });
1482
1615
  const trackId = this.localTrackIds.get("camera");
1483
1616
  if (trackId) {
1484
1617
  this.send({
@@ -1490,6 +1623,16 @@ var SFUClient = class {
1490
1623
  });
1491
1624
  }
1492
1625
  }
1626
+ sendChatMessage(input) {
1627
+ return this.send({
1628
+ type: "send_chat",
1629
+ payload: {
1630
+ message: input.message,
1631
+ reply_to: input.reply_to ?? null,
1632
+ target: input.target ?? null
1633
+ }
1634
+ });
1635
+ }
1493
1636
  async startRecording() {
1494
1637
  if (!this.send({ type: "start_recording" })) {
1495
1638
  throw new Error("SFU signaling is not connected");
@@ -1541,6 +1684,12 @@ var SFUClient = class {
1541
1684
  );
1542
1685
  return true;
1543
1686
  }
1687
+ sendReaction(reaction) {
1688
+ this.send({
1689
+ type: "send_reaction",
1690
+ reaction
1691
+ });
1692
+ }
1544
1693
  removeRemoteParticipant(participantId) {
1545
1694
  const stream = this.remoteStreams.get(participantId);
1546
1695
  if (stream) {
@@ -1957,6 +2106,44 @@ var VideoSDKCore = class {
1957
2106
  console.warn(`[SFU] Reconnecting, attempt ${attempt}`);
1958
2107
  },
1959
2108
  // ADMISSION CONTROL
2109
+ onRoomUpdate: (payload) => {
2110
+ const participants = payload?.participants || [];
2111
+ const local = participants.find(
2112
+ (p) => p?.participant_id === this.myId
2113
+ );
2114
+ if (local) {
2115
+ const media = this.state.localParticipant?.media;
2116
+ this.state.updateLocalParticipant({
2117
+ id: this.myId,
2118
+ name: this.participantName,
2119
+ media: {
2120
+ micEnabled: !Boolean(local.audio_muted),
2121
+ camEnabled: !Boolean(local.video_muted),
2122
+ isScreenSharing: Boolean(local.screen_sharing),
2123
+ screenStream: this.screenStream,
2124
+ screenTrack: this.screenStream?.getVideoTracks()[0]
2125
+ }
2126
+ });
2127
+ if (this.localStream) {
2128
+ this.localStream.getAudioTracks().forEach((track) => {
2129
+ track.enabled = !Boolean(local.audio_muted);
2130
+ });
2131
+ this.localStream.getVideoTracks().forEach((track) => {
2132
+ track.enabled = !Boolean(local.video_muted);
2133
+ });
2134
+ }
2135
+ if (local.screen_sharing) this.state.setPresenterId(this.myId);
2136
+ else if (this.state.presenterId === this.myId)
2137
+ this.state.setPresenterId(null);
2138
+ }
2139
+ const presenter = participants.find((p) => p?.screen_sharing);
2140
+ if (presenter) this.state.setPresenterId(presenter.participant_id);
2141
+ else if (!local?.screen_sharing) this.state.setPresenterId(null);
2142
+ for (const message of payload?.chat_history || []) {
2143
+ this.state.addChatMessage(message);
2144
+ }
2145
+ this.events.onRoomUpdate?.(payload);
2146
+ },
1960
2147
  onJoinPending: (info) => {
1961
2148
  console.log("[SFU] Join request pending", info);
1962
2149
  },
@@ -1991,6 +2178,19 @@ var VideoSDKCore = class {
1991
2178
  false
1992
2179
  );
1993
2180
  },
2181
+ onChatMessage: (message) => {
2182
+ const msg = {
2183
+ id: message.id,
2184
+ text: message.text,
2185
+ sender_id: message.sender_id,
2186
+ sender_name: message.sender_name,
2187
+ timestamp: Number(message.timestamp),
2188
+ reply_to: message.reply_to ?? null,
2189
+ target: message.target ?? null
2190
+ };
2191
+ this.state.addChatMessage(msg);
2192
+ this.events.onChatMessage?.(msg);
2193
+ },
1994
2194
  onJoinRequested: (request) => {
1995
2195
  const requestId = request?.request_id || request?.requestId;
1996
2196
  const participantId = request?.user_id || request?.participantId;
@@ -2059,6 +2259,17 @@ var VideoSDKCore = class {
2059
2259
  this.events.onUserLeft?.(pid);
2060
2260
  },
2061
2261
  // TRACKS
2262
+ onParticipantMediaState: (participantId, audioMuted, videoMuted) => {
2263
+ if (participantId === this.myId) return;
2264
+ const participant = this.state.getParticipant(participantId);
2265
+ if (!participant) return;
2266
+ this.state.updateParticipantMedia(participantId, {
2267
+ micEnabled: !audioMuted,
2268
+ camEnabled: !videoMuted
2269
+ });
2270
+ this.events.onMuteStateChanged?.(participantId, "audio", audioMuted);
2271
+ this.events.onMuteStateChanged?.(participantId, "video", videoMuted);
2272
+ },
2062
2273
  onTrackPublished: (track) => {
2063
2274
  console.log("[SFU] Track published", track);
2064
2275
  },
@@ -2128,6 +2339,9 @@ var VideoSDKCore = class {
2128
2339
  // SCREEN SHARE
2129
2340
  onScreenShareStarted: (participantId, _track) => {
2130
2341
  console.log("[SFU] Screen share started", participantId);
2342
+ if (participantId === this.myId && !this.screenStream) {
2343
+ this.screenStream = this.sfuClient?.getScreenStream() ?? null;
2344
+ }
2131
2345
  this.state.setPresenterId(participantId);
2132
2346
  if (participantId === this.myId) {
2133
2347
  this.state.updateLocalParticipant({
@@ -2351,7 +2565,7 @@ var VideoSDKCore = class {
2351
2565
  media: {
2352
2566
  isScreenSharing: true,
2353
2567
  screenStream: this.screenStream,
2354
- screenTrack: this.screenStream.getVideoTracks()[0]
2568
+ screenTrack: this.screenStream?.getVideoTracks()[0]
2355
2569
  }
2356
2570
  });
2357
2571
  return this.screenStream;
@@ -2408,18 +2622,20 @@ var VideoSDKCore = class {
2408
2622
  console.warn("SFU Client not connected");
2409
2623
  return;
2410
2624
  }
2411
- const senderName = this.state.localParticipant?.name || "Anonymous";
2412
- const msg = {
2413
- id: crypto.randomUUID(),
2414
- sender_id: this.myId,
2415
- sender_name: senderName,
2416
- text: payload.message.trim(),
2417
- timestamp: Date.now(),
2625
+ const message = payload.message.trim();
2626
+ if (!message) return;
2627
+ this.sfuClient.sendChatMessage({
2628
+ message,
2418
2629
  reply_to: payload.reply_to ?? null,
2419
2630
  target: payload.target ?? null
2420
- };
2421
- this.state.addChatMessage(msg);
2422
- this.events.onChatMessage?.(msg);
2631
+ });
2632
+ }
2633
+ sendReaction(reaction) {
2634
+ if (!this.sfuClient) {
2635
+ console.warn("SFU Client not connected");
2636
+ return;
2637
+ }
2638
+ this.sfuClient.sendReaction(reaction);
2423
2639
  }
2424
2640
  // CLEANUP
2425
2641
  cleanupRemoteParticipant(participantId) {
@@ -2507,6 +2723,7 @@ var MeetingProvider = ({
2507
2723
  const trackStateListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
2508
2724
  const recordingStartedListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
2509
2725
  const recordingStoppedListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
2726
+ const reactionListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
2510
2727
  const activeSpeakerListeners = (0, import_react2.useRef)(
2511
2728
  /* @__PURE__ */ new Set()
2512
2729
  );
@@ -2536,6 +2753,9 @@ var MeetingProvider = ({
2536
2753
  },
2537
2754
  onRecordingStopped: (recording) => {
2538
2755
  recordingStoppedListeners.current.forEach((fn) => fn(recording));
2756
+ },
2757
+ onReaction: (reaction) => {
2758
+ reactionListeners.current.forEach((fn) => fn(reaction));
2539
2759
  }
2540
2760
  }
2541
2761
  );
@@ -2582,6 +2802,7 @@ var MeetingProvider = ({
2582
2802
  startScreenShare: sdk.startScreenShare.bind(sdk),
2583
2803
  stopScreenShare: sdk.stopScreenShare.bind(sdk),
2584
2804
  sendMessage: sdk.sendChatMessage.bind(sdk),
2805
+ sendReaction: sdk.sendReaction.bind(sdk),
2585
2806
  room: sdk.getMeeting(),
2586
2807
  localParticipant,
2587
2808
  participants,
@@ -2638,6 +2859,10 @@ var MeetingProvider = ({
2638
2859
  recordingStoppedListeners.current.add(cb);
2639
2860
  return () => recordingStoppedListeners.current.delete(cb);
2640
2861
  },
2862
+ onReaction: (cb) => {
2863
+ reactionListeners.current.add(cb);
2864
+ return () => reactionListeners.current.delete(cb);
2865
+ },
2641
2866
  onActiveSpeakersChanged: (cb) => {
2642
2867
  activeSpeakerListeners.current.add(cb);
2643
2868
  return () => {
@@ -2734,6 +2959,10 @@ var useMeeting = (handlers) => {
2734
2959
  if (!handlers?.onRecordingStopped) return;
2735
2960
  return ctx.onRecordingStopped(handlers.onRecordingStopped);
2736
2961
  }, [handlers?.onRecordingStopped]);
2962
+ (0, import_react4.useEffect)(() => {
2963
+ if (!handlers?.onReaction) return;
2964
+ return ctx.onReaction(handlers.onReaction);
2965
+ }, [handlers?.onReaction]);
2737
2966
  const { sdk: _, ...publicApi } = ctx;
2738
2967
  return publicApi;
2739
2968
  };