@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.mjs CHANGED
@@ -357,6 +357,7 @@ var SFUClient = class {
357
357
  this.iceServers = [];
358
358
  this.requestId = 0;
359
359
  this.publisherRevision = 0;
360
+ this.screenShareGeneration = 0;
360
361
  this.currentParticipantId = null;
361
362
  this.token = null;
362
363
  this.room = null;
@@ -367,6 +368,8 @@ var SFUClient = class {
367
368
  this.intentionalClose = false;
368
369
  this.leaveAcknowledged = false;
369
370
  this.joiningWithResume = false;
371
+ this.desiredAudioMuted = false;
372
+ this.desiredVideoMuted = false;
370
373
  this.reconnectTimer = null;
371
374
  this.reconnectAttempt = 0;
372
375
  this.maxReconnectAttempts = 8;
@@ -435,6 +438,14 @@ var SFUClient = class {
435
438
  this.onRecordingStopped = options.onRecordingStopped || (() => {
436
439
  });
437
440
  this.onError = options.onError || console.error;
441
+ this.onChatMessage = options.onChatMessage || (() => {
442
+ });
443
+ this.onReaction = options.onReaction || (() => {
444
+ });
445
+ this.onRoomUpdate = options.onRoomUpdate || (() => {
446
+ });
447
+ this.onParticipantMediaState = options.onParticipantMediaState || (() => {
448
+ });
438
449
  this.autoSubscribe = options.autoSubscribe !== false;
439
450
  }
440
451
  startHeartbeat() {
@@ -557,6 +568,10 @@ var SFUClient = class {
557
568
  avatarUrl: options.avatarUrl
558
569
  };
559
570
  if (options.participantId) this.participantId = options.participantId;
571
+ if (typeof options.audioMuted === "boolean")
572
+ this.desiredAudioMuted = options.audioMuted;
573
+ if (typeof options.videoMuted === "boolean")
574
+ this.desiredVideoMuted = options.videoMuted;
560
575
  if (!this.resumeToken) {
561
576
  this.resumeToken = this.loadResumeToken(this.room);
562
577
  }
@@ -623,7 +638,9 @@ var SFUClient = class {
623
638
  room_code: this.room,
624
639
  resume_token: this.resumeToken,
625
640
  display_name: options.displayName,
626
- avatar_url: options.avatarUrl
641
+ avatar_url: options.avatarUrl,
642
+ audio_muted: this.desiredAudioMuted,
643
+ video_muted: this.desiredVideoMuted
627
644
  };
628
645
  if (this.pendingJoinRequestId) {
629
646
  this.send({
@@ -798,6 +815,16 @@ var SFUClient = class {
798
815
  this.onParticipantLeft(participantId);
799
816
  break;
800
817
  }
818
+ case "participant_media_state": {
819
+ const participantId = payload.participant_id;
820
+ if (!participantId) break;
821
+ this.onParticipantMediaState(
822
+ participantId,
823
+ Boolean(payload.audio_muted),
824
+ Boolean(payload.video_muted)
825
+ );
826
+ break;
827
+ }
801
828
  case "participant_presence":
802
829
  case "participant_presence_changed": {
803
830
  const participantId = payload.participant_id;
@@ -920,6 +947,50 @@ var SFUClient = class {
920
947
  this.onScreenShareStopped(participantId, trackId);
921
948
  break;
922
949
  }
950
+ case "room_state": {
951
+ const participants = payload.participants || [];
952
+ this.onRoomStateReconciled(payload);
953
+ this.onRoomUpdate(payload);
954
+ for (const participant of participants) {
955
+ const pid = participant?.participant_id;
956
+ if (!pid) continue;
957
+ if (pid !== this.currentParticipantId) {
958
+ this.participants.set(pid, participant);
959
+ this.onParticipantPresence(
960
+ pid,
961
+ String(participant.presence || "connected")
962
+ );
963
+ this.onParticipantMediaState(
964
+ pid,
965
+ Boolean(participant.audio_muted),
966
+ Boolean(participant.video_muted)
967
+ );
968
+ }
969
+ }
970
+ for (const [pid] of this.participants) {
971
+ if (!participants.some((p) => p?.participant_id === pid)) {
972
+ this.participants.delete(pid);
973
+ this.removeRemoteParticipant(pid);
974
+ this.onParticipantLeft(pid);
975
+ }
976
+ }
977
+ for (const track of payload.tracks || []) this.rememberTrack(track);
978
+ if (Array.isArray(payload.chat_history)) {
979
+ for (const message2 of payload.chat_history)
980
+ this.onChatMessage(message2);
981
+ }
982
+ break;
983
+ }
984
+ case "chat_message": {
985
+ const msg = payload.message || payload;
986
+ if (msg?.id) this.onChatMessage(msg);
987
+ break;
988
+ }
989
+ case "reaction": {
990
+ const reaction = payload.reaction || payload;
991
+ if (reaction?.id && reaction?.emoji) this.onReaction(reaction);
992
+ break;
993
+ }
923
994
  case "error": {
924
995
  const message2 = payload.message || "SFU signaling error";
925
996
  if (this.state === "reconnecting" && message2.includes("participant session is stale")) {
@@ -942,6 +1013,16 @@ var SFUClient = class {
942
1013
  console.debug("[SFU] Ignoring unknown server message", message);
943
1014
  }
944
1015
  }
1016
+ onRoomStateReconciled(payload) {
1017
+ const participants = payload.participants || [];
1018
+ const local = participants.find(
1019
+ (p) => p?.participant_id === this.currentParticipantId
1020
+ );
1021
+ if (local) {
1022
+ this.desiredAudioMuted = Boolean(local.audio_muted);
1023
+ this.desiredVideoMuted = Boolean(local.video_muted);
1024
+ }
1025
+ }
945
1026
  rememberTrack(track) {
946
1027
  this.trackMetadata.set(track.track_id, track);
947
1028
  this.trackToParticipant.set(track.track_id, track.participant_id);
@@ -977,8 +1058,21 @@ var SFUClient = class {
977
1058
  for (const participant of this.participants.values()) {
978
1059
  if (participant.participant_id !== this.currentParticipantId) {
979
1060
  this.onParticipantJoined(participant);
1061
+ this.onParticipantPresence(
1062
+ participant.participant_id,
1063
+ String(participant.presence || "connected")
1064
+ );
1065
+ this.onParticipantMediaState(
1066
+ participant.participant_id,
1067
+ Boolean(participant.audio_muted),
1068
+ Boolean(participant.video_muted)
1069
+ );
980
1070
  }
981
1071
  }
1072
+ if (Array.isArray(payload.chat_history)) {
1073
+ for (const message of payload.chat_history) this.onChatMessage(message);
1074
+ }
1075
+ this.onRoomUpdate(payload);
982
1076
  if (this.localStream) {
983
1077
  await this.publishExistingTracks();
984
1078
  }
@@ -1337,9 +1431,27 @@ var SFUClient = class {
1337
1431
  if (!this.screenTransceiver || !this.screenSender) {
1338
1432
  throw new Error("Screen transceiver is not initialized");
1339
1433
  }
1340
- if (this.screenStream) {
1434
+ const existingTrack = this.screenSender.track;
1435
+ if (this.screenStream && existingTrack && existingTrack.readyState === "live") {
1341
1436
  return this.screenStream;
1342
1437
  }
1438
+ const generation = ++this.screenShareGeneration;
1439
+ try {
1440
+ await this.screenSender.replaceTrack(null);
1441
+ } catch (err) {
1442
+ console.warn("[SFU] Failed to clear previous screen track", err);
1443
+ }
1444
+ if (this.screenStream) {
1445
+ for (const track of this.screenStream.getTracks()) {
1446
+ try {
1447
+ track.onended = null;
1448
+ track.stop();
1449
+ } catch {
1450
+ }
1451
+ }
1452
+ }
1453
+ this.screenStream = null;
1454
+ this.screenTransceiver.direction = "inactive";
1343
1455
  const screenStream = await navigator.mediaDevices.getDisplayMedia({
1344
1456
  video: true,
1345
1457
  audio: false
@@ -1351,83 +1463,99 @@ var SFUClient = class {
1351
1463
  }
1352
1464
  this.screenStream = screenStream;
1353
1465
  try {
1354
- if (!this.send({ type: "start_screen_share" })) {
1355
- throw new Error("Failed to signal start_screen_share");
1356
- }
1357
1466
  await this.screenSender.replaceTrack(screenTrack);
1358
1467
  this.screenTransceiver.direction = "sendonly";
1468
+ this.send({
1469
+ type: "start_screen_share"
1470
+ });
1471
+ await new Promise((resolve) => {
1472
+ queueMicrotask(resolve);
1473
+ });
1359
1474
  const offer = await this.publisher.createOffer();
1360
1475
  await this.publisher.setLocalDescription(offer);
1476
+ const localDescription = this.publisher.localDescription;
1477
+ if (!localDescription?.sdp) {
1478
+ throw new Error("Publisher did not produce a valid screen-share SDP");
1479
+ }
1480
+ const revision = ++this.publisherRevision;
1361
1481
  this.send({
1362
1482
  type: "publish_offer",
1363
1483
  payload: {
1364
- revision: ++this.publisherRevision,
1365
- sdp: offer.sdp
1484
+ revision,
1485
+ sdp: localDescription.sdp
1366
1486
  }
1367
1487
  });
1368
1488
  screenTrack.onended = () => {
1489
+ if (generation !== this.screenShareGeneration) {
1490
+ return;
1491
+ }
1369
1492
  void this.stopScreenShare();
1370
1493
  };
1371
- console.info("[publisher] Screen track attached", {
1372
- screenTrackId: screenTrack.id,
1373
- cameraTrackId: this.videoSender?.track?.id,
1374
- screenMid: this.screenTransceiver.mid,
1375
- cameraMid: this.cameraTransceiver?.mid
1376
- });
1377
- return this.screenStream;
1494
+ return screenStream;
1378
1495
  } catch (error) {
1379
- try {
1380
- await this.screenSender.replaceTrack(null);
1381
- } catch {
1382
- }
1383
- this.screenTransceiver.direction = "inactive";
1384
- screenStream.getTracks().forEach((track) => track.stop());
1385
- this.screenStream = null;
1386
- try {
1387
- this.send({ type: "stop_screen_share" });
1388
- } catch {
1496
+ if (generation === this.screenShareGeneration) {
1497
+ try {
1498
+ await this.screenSender.replaceTrack(null);
1499
+ } catch {
1500
+ }
1501
+ this.screenTransceiver.direction = "inactive";
1502
+ screenTrack.onended = null;
1503
+ screenStream.getTracks().forEach((track) => {
1504
+ try {
1505
+ track.stop();
1506
+ } catch {
1507
+ }
1508
+ });
1509
+ this.screenStream = null;
1510
+ try {
1511
+ this.send({
1512
+ type: "stop_screen_share"
1513
+ });
1514
+ } catch {
1515
+ }
1389
1516
  }
1390
1517
  throw error;
1391
1518
  }
1392
1519
  }
1393
1520
  async stopScreenShare() {
1394
- if (!this.screenStream && !this.screenSender?.track) return;
1395
- const screenTrack = this.screenSender?.track || null;
1396
- if (screenTrack) screenTrack.onended = null;
1521
+ const stream = this.screenStream;
1522
+ const senderTrack = this.screenSender?.track || null;
1523
+ if (!stream && !senderTrack) return;
1524
+ ++this.screenShareGeneration;
1525
+ if (senderTrack) senderTrack.onended = null;
1397
1526
  try {
1398
1527
  await this.screenSender?.replaceTrack(null);
1399
1528
  } catch (error) {
1400
1529
  this.onError(error);
1401
1530
  }
1402
- if (this.screenTransceiver) {
1403
- this.screenTransceiver.direction = "inactive";
1404
- }
1405
- this.send({ type: "stop_screen_share" });
1531
+ if (this.screenTransceiver) this.screenTransceiver.direction = "inactive";
1406
1532
  try {
1407
1533
  const offer = await this.publisher?.createOffer();
1408
1534
  if (offer && this.publisher) {
1409
1535
  await this.publisher.setLocalDescription(offer);
1410
1536
  this.send({
1411
1537
  type: "publish_offer",
1412
- payload: {
1413
- revision: ++this.publisherRevision,
1414
- sdp: offer.sdp
1415
- }
1538
+ payload: { revision: ++this.publisherRevision, sdp: offer.sdp }
1416
1539
  });
1417
1540
  }
1418
1541
  } catch (error) {
1419
1542
  this.onError(error);
1420
1543
  }
1421
- this.screenStream?.getTracks().forEach((track) => track.stop());
1544
+ this.send({ type: "stop_screen_share" });
1545
+ stream?.getTracks().forEach((track) => track.stop());
1422
1546
  this.screenStream = null;
1423
- if (this.currentParticipantId) {
1547
+ if (this.currentParticipantId)
1424
1548
  this.onScreenShareStopped(this.currentParticipantId);
1425
- }
1426
1549
  }
1427
1550
  muteAudio(muted) {
1551
+ this.desiredAudioMuted = muted;
1428
1552
  this.localStream?.getAudioTracks().forEach((track) => {
1429
1553
  track.enabled = !muted;
1430
1554
  });
1555
+ this.send({
1556
+ type: "set_media_state",
1557
+ payload: { audio_muted: muted, video_muted: this.desiredVideoMuted }
1558
+ });
1431
1559
  const trackId = this.localTrackIds.get("audio");
1432
1560
  if (trackId) {
1433
1561
  this.send({
@@ -1440,9 +1568,14 @@ var SFUClient = class {
1440
1568
  }
1441
1569
  }
1442
1570
  muteVideo(muted) {
1571
+ this.desiredVideoMuted = muted;
1443
1572
  this.localStream?.getVideoTracks().forEach((track) => {
1444
1573
  track.enabled = !muted;
1445
1574
  });
1575
+ this.send({
1576
+ type: "set_media_state",
1577
+ payload: { audio_muted: this.desiredAudioMuted, video_muted: muted }
1578
+ });
1446
1579
  const trackId = this.localTrackIds.get("camera");
1447
1580
  if (trackId) {
1448
1581
  this.send({
@@ -1454,6 +1587,16 @@ var SFUClient = class {
1454
1587
  });
1455
1588
  }
1456
1589
  }
1590
+ sendChatMessage(input) {
1591
+ return this.send({
1592
+ type: "send_chat",
1593
+ payload: {
1594
+ message: input.message,
1595
+ reply_to: input.reply_to ?? null,
1596
+ target: input.target ?? null
1597
+ }
1598
+ });
1599
+ }
1457
1600
  async startRecording() {
1458
1601
  if (!this.send({ type: "start_recording" })) {
1459
1602
  throw new Error("SFU signaling is not connected");
@@ -1505,6 +1648,12 @@ var SFUClient = class {
1505
1648
  );
1506
1649
  return true;
1507
1650
  }
1651
+ sendReaction(reaction) {
1652
+ this.send({
1653
+ type: "send_reaction",
1654
+ reaction
1655
+ });
1656
+ }
1508
1657
  removeRemoteParticipant(participantId) {
1509
1658
  const stream = this.remoteStreams.get(participantId);
1510
1659
  if (stream) {
@@ -1921,6 +2070,44 @@ var VideoSDKCore = class {
1921
2070
  console.warn(`[SFU] Reconnecting, attempt ${attempt}`);
1922
2071
  },
1923
2072
  // ADMISSION CONTROL
2073
+ onRoomUpdate: (payload) => {
2074
+ const participants = payload?.participants || [];
2075
+ const local = participants.find(
2076
+ (p) => p?.participant_id === this.myId
2077
+ );
2078
+ if (local) {
2079
+ const media = this.state.localParticipant?.media;
2080
+ this.state.updateLocalParticipant({
2081
+ id: this.myId,
2082
+ name: this.participantName,
2083
+ media: {
2084
+ micEnabled: !Boolean(local.audio_muted),
2085
+ camEnabled: !Boolean(local.video_muted),
2086
+ isScreenSharing: Boolean(local.screen_sharing),
2087
+ screenStream: this.screenStream,
2088
+ screenTrack: this.screenStream?.getVideoTracks()[0]
2089
+ }
2090
+ });
2091
+ if (this.localStream) {
2092
+ this.localStream.getAudioTracks().forEach((track) => {
2093
+ track.enabled = !Boolean(local.audio_muted);
2094
+ });
2095
+ this.localStream.getVideoTracks().forEach((track) => {
2096
+ track.enabled = !Boolean(local.video_muted);
2097
+ });
2098
+ }
2099
+ if (local.screen_sharing) this.state.setPresenterId(this.myId);
2100
+ else if (this.state.presenterId === this.myId)
2101
+ this.state.setPresenterId(null);
2102
+ }
2103
+ const presenter = participants.find((p) => p?.screen_sharing);
2104
+ if (presenter) this.state.setPresenterId(presenter.participant_id);
2105
+ else if (!local?.screen_sharing) this.state.setPresenterId(null);
2106
+ for (const message of payload?.chat_history || []) {
2107
+ this.state.addChatMessage(message);
2108
+ }
2109
+ this.events.onRoomUpdate?.(payload);
2110
+ },
1924
2111
  onJoinPending: (info) => {
1925
2112
  console.log("[SFU] Join request pending", info);
1926
2113
  },
@@ -1955,6 +2142,19 @@ var VideoSDKCore = class {
1955
2142
  false
1956
2143
  );
1957
2144
  },
2145
+ onChatMessage: (message) => {
2146
+ const msg = {
2147
+ id: message.id,
2148
+ text: message.text,
2149
+ sender_id: message.sender_id,
2150
+ sender_name: message.sender_name,
2151
+ timestamp: Number(message.timestamp),
2152
+ reply_to: message.reply_to ?? null,
2153
+ target: message.target ?? null
2154
+ };
2155
+ this.state.addChatMessage(msg);
2156
+ this.events.onChatMessage?.(msg);
2157
+ },
1958
2158
  onJoinRequested: (request) => {
1959
2159
  const requestId = request?.request_id || request?.requestId;
1960
2160
  const participantId = request?.user_id || request?.participantId;
@@ -2023,6 +2223,17 @@ var VideoSDKCore = class {
2023
2223
  this.events.onUserLeft?.(pid);
2024
2224
  },
2025
2225
  // TRACKS
2226
+ onParticipantMediaState: (participantId, audioMuted, videoMuted) => {
2227
+ if (participantId === this.myId) return;
2228
+ const participant = this.state.getParticipant(participantId);
2229
+ if (!participant) return;
2230
+ this.state.updateParticipantMedia(participantId, {
2231
+ micEnabled: !audioMuted,
2232
+ camEnabled: !videoMuted
2233
+ });
2234
+ this.events.onMuteStateChanged?.(participantId, "audio", audioMuted);
2235
+ this.events.onMuteStateChanged?.(participantId, "video", videoMuted);
2236
+ },
2026
2237
  onTrackPublished: (track) => {
2027
2238
  console.log("[SFU] Track published", track);
2028
2239
  },
@@ -2092,6 +2303,9 @@ var VideoSDKCore = class {
2092
2303
  // SCREEN SHARE
2093
2304
  onScreenShareStarted: (participantId, _track) => {
2094
2305
  console.log("[SFU] Screen share started", participantId);
2306
+ if (participantId === this.myId && !this.screenStream) {
2307
+ this.screenStream = this.sfuClient?.getScreenStream() ?? null;
2308
+ }
2095
2309
  this.state.setPresenterId(participantId);
2096
2310
  if (participantId === this.myId) {
2097
2311
  this.state.updateLocalParticipant({
@@ -2315,7 +2529,7 @@ var VideoSDKCore = class {
2315
2529
  media: {
2316
2530
  isScreenSharing: true,
2317
2531
  screenStream: this.screenStream,
2318
- screenTrack: this.screenStream.getVideoTracks()[0]
2532
+ screenTrack: this.screenStream?.getVideoTracks()[0]
2319
2533
  }
2320
2534
  });
2321
2535
  return this.screenStream;
@@ -2372,18 +2586,20 @@ var VideoSDKCore = class {
2372
2586
  console.warn("SFU Client not connected");
2373
2587
  return;
2374
2588
  }
2375
- const senderName = this.state.localParticipant?.name || "Anonymous";
2376
- const msg = {
2377
- id: crypto.randomUUID(),
2378
- sender_id: this.myId,
2379
- sender_name: senderName,
2380
- text: payload.message.trim(),
2381
- timestamp: Date.now(),
2589
+ const message = payload.message.trim();
2590
+ if (!message) return;
2591
+ this.sfuClient.sendChatMessage({
2592
+ message,
2382
2593
  reply_to: payload.reply_to ?? null,
2383
2594
  target: payload.target ?? null
2384
- };
2385
- this.state.addChatMessage(msg);
2386
- this.events.onChatMessage?.(msg);
2595
+ });
2596
+ }
2597
+ sendReaction(reaction) {
2598
+ if (!this.sfuClient) {
2599
+ console.warn("SFU Client not connected");
2600
+ return;
2601
+ }
2602
+ this.sfuClient.sendReaction(reaction);
2387
2603
  }
2388
2604
  // CLEANUP
2389
2605
  cleanupRemoteParticipant(participantId) {
@@ -2471,6 +2687,7 @@ var MeetingProvider = ({
2471
2687
  const trackStateListeners = useRef(/* @__PURE__ */ new Set());
2472
2688
  const recordingStartedListeners = useRef(/* @__PURE__ */ new Set());
2473
2689
  const recordingStoppedListeners = useRef(/* @__PURE__ */ new Set());
2690
+ const reactionListeners = useRef(/* @__PURE__ */ new Set());
2474
2691
  const activeSpeakerListeners = useRef(
2475
2692
  /* @__PURE__ */ new Set()
2476
2693
  );
@@ -2500,6 +2717,9 @@ var MeetingProvider = ({
2500
2717
  },
2501
2718
  onRecordingStopped: (recording) => {
2502
2719
  recordingStoppedListeners.current.forEach((fn) => fn(recording));
2720
+ },
2721
+ onReaction: (reaction) => {
2722
+ reactionListeners.current.forEach((fn) => fn(reaction));
2503
2723
  }
2504
2724
  }
2505
2725
  );
@@ -2546,6 +2766,7 @@ var MeetingProvider = ({
2546
2766
  startScreenShare: sdk.startScreenShare.bind(sdk),
2547
2767
  stopScreenShare: sdk.stopScreenShare.bind(sdk),
2548
2768
  sendMessage: sdk.sendChatMessage.bind(sdk),
2769
+ sendReaction: sdk.sendReaction.bind(sdk),
2549
2770
  room: sdk.getMeeting(),
2550
2771
  localParticipant,
2551
2772
  participants,
@@ -2602,6 +2823,10 @@ var MeetingProvider = ({
2602
2823
  recordingStoppedListeners.current.add(cb);
2603
2824
  return () => recordingStoppedListeners.current.delete(cb);
2604
2825
  },
2826
+ onReaction: (cb) => {
2827
+ reactionListeners.current.add(cb);
2828
+ return () => reactionListeners.current.delete(cb);
2829
+ },
2605
2830
  onActiveSpeakersChanged: (cb) => {
2606
2831
  activeSpeakerListeners.current.add(cb);
2607
2832
  return () => {
@@ -2698,6 +2923,10 @@ var useMeeting = (handlers) => {
2698
2923
  if (!handlers?.onRecordingStopped) return;
2699
2924
  return ctx.onRecordingStopped(handlers.onRecordingStopped);
2700
2925
  }, [handlers?.onRecordingStopped]);
2926
+ useEffect3(() => {
2927
+ if (!handlers?.onReaction) return;
2928
+ return ctx.onReaction(handlers.onReaction);
2929
+ }, [handlers?.onReaction]);
2701
2930
  const { sdk: _, ...publicApi } = ctx;
2702
2931
  return publicApi;
2703
2932
  };