@defcomm/meetingsdk 0.5.5 → 0.5.6

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
@@ -197,6 +197,8 @@ var MeetingState = class {
197
197
  this.presenterId = null;
198
198
  this.activeSpeakerIds = /* @__PURE__ */ new Set();
199
199
  this.recording = null;
200
+ this.liveTranscripts = [];
201
+ this.networkQuality = null;
200
202
  this.listeners = /* @__PURE__ */ new Map();
201
203
  }
202
204
  // ---- reactive system ----
@@ -234,6 +236,21 @@ var MeetingState = class {
234
236
  this.recording = null;
235
237
  this.notify("recording");
236
238
  }
239
+ addLiveTranscript(transcript) {
240
+ this.liveTranscripts = [...this.liveTranscripts.slice(-499), transcript];
241
+ this.notify("transcript");
242
+ }
243
+ getLiveTranscripts() {
244
+ return this.liveTranscripts;
245
+ }
246
+ clearLiveTranscripts() {
247
+ this.liveTranscripts = [];
248
+ this.notify("transcript");
249
+ }
250
+ setNetworkQuality(quality) {
251
+ this.networkQuality = quality;
252
+ this.notify("networkQuality");
253
+ }
237
254
  // ---- participants ----
238
255
  addParticipant(p) {
239
256
  if (this.participants.has(p.id)) return false;
@@ -354,10 +371,14 @@ var MeetingState = class {
354
371
  this.chatMessages.clear();
355
372
  this.presenterId = null;
356
373
  this.activeSpeakerIds = /* @__PURE__ */ new Set();
374
+ this.liveTranscripts = [];
375
+ this.networkQuality = null;
357
376
  this.notify("participants");
358
377
  this.notify("chat");
359
378
  this.notify("presenter");
360
379
  this.notify("activeSpeakers");
380
+ this.notify("transcript");
381
+ this.notify("networkQuality");
361
382
  }
362
383
  };
363
384
 
@@ -419,6 +440,12 @@ var SFUClient = class {
419
440
  this.trackToSource = /* @__PURE__ */ new Map();
420
441
  this.localTrackIds = /* @__PURE__ */ new Map();
421
442
  this.subscriberStatsTimer = null;
443
+ this.liveTranscriptionActive = false;
444
+ this.liveTranscriptionSequence = 0;
445
+ this.liveTranscriptionStartedAt = 0;
446
+ this.liveTranscriptionRecorder = null;
447
+ this.liveTranscriptionLoopPromise = null;
448
+ this.networkProfile = null;
422
449
  this.heartbeatTimer = null;
423
450
  this.apiBase = options.apiBase || "https://api.example.com/api";
424
451
  this.wsBase = options.wsBase || "wss://api.example.com/ws";
@@ -475,6 +502,10 @@ var SFUClient = class {
475
502
  });
476
503
  this.onRoomUpdate = options.onRoomUpdate || (() => {
477
504
  });
505
+ this.onLiveTranscript = options.onLiveTranscript || (() => {
506
+ });
507
+ this.onNetworkQuality = options.onNetworkQuality || (() => {
508
+ });
478
509
  this.onParticipantMediaState = options.onParticipantMediaState || (() => {
479
510
  });
480
511
  this.autoSubscribe = options.autoSubscribe !== false;
@@ -1069,6 +1100,40 @@ var SFUClient = class {
1069
1100
  if (reaction?.id && reaction?.emoji) this.onReaction(reaction);
1070
1101
  break;
1071
1102
  }
1103
+ case "live_transcript": {
1104
+ const transcript = {
1105
+ transcript_id: String(payload.transcript_id),
1106
+ participant_id: String(payload.participant_id),
1107
+ speaker_name: payload.speaker_name ?? null,
1108
+ sequence: Number(payload.sequence ?? 0),
1109
+ text: String(payload.text ?? ""),
1110
+ offset_ms: payload.offset_ms == null ? null : Number(payload.offset_ms),
1111
+ start_ms: payload.start_ms == null ? null : Number(payload.start_ms),
1112
+ end_ms: payload.end_ms == null ? null : Number(payload.end_ms),
1113
+ timestamp: String(payload.timestamp ?? (/* @__PURE__ */ new Date()).toISOString())
1114
+ };
1115
+ if (transcript.text.trim()) this.onLiveTranscript(transcript);
1116
+ break;
1117
+ }
1118
+ case "network_quality": {
1119
+ const raw = payload.quality || payload;
1120
+ const quality = {
1121
+ participant_id: String(raw.participant_id ?? this.currentParticipantId ?? this.participantId),
1122
+ quality: String(raw.quality ?? "unknown"),
1123
+ score: Number(raw.score ?? 0),
1124
+ recommended_action: String(raw.recommended_action ?? "maintain"),
1125
+ packet_loss_percent: Number(raw.packet_loss_percent ?? 0),
1126
+ upload_kbps: Number(raw.upload_kbps ?? 0),
1127
+ forwarded_drop_percent: Number(raw.forwarded_drop_percent ?? 0),
1128
+ queued_packets: Number(raw.queued_packets ?? 0),
1129
+ packets_received: Number(raw.packets_received ?? 0),
1130
+ packets_lost: Number(raw.packets_lost ?? 0),
1131
+ observed_at: String(raw.observed_at ?? (/* @__PURE__ */ new Date()).toISOString())
1132
+ };
1133
+ this.onNetworkQuality(quality);
1134
+ void this.applyNetworkRecommendation(quality.recommended_action);
1135
+ break;
1136
+ }
1072
1137
  case "error": {
1073
1138
  const message2 = payload.message || "SFU signaling error";
1074
1139
  if (this.state === "reconnecting" && message2.includes("participant session is stale")) {
@@ -1728,6 +1793,147 @@ var SFUClient = class {
1728
1793
  }
1729
1794
  });
1730
1795
  }
1796
+ getLiveTranscriptionMimeType() {
1797
+ if (typeof MediaRecorder === "undefined") {
1798
+ throw new Error("MediaRecorder is not supported by this browser");
1799
+ }
1800
+ const candidates = [
1801
+ "audio/webm;codecs=opus",
1802
+ "audio/webm",
1803
+ "audio/ogg;codecs=opus"
1804
+ ];
1805
+ return candidates.find((type) => MediaRecorder.isTypeSupported(type)) || "";
1806
+ }
1807
+ async startLiveTranscription(audioTrack, speakerName) {
1808
+ if (this.liveTranscriptionActive) return;
1809
+ if (!this.room) throw new Error("Cannot start live transcription without a room");
1810
+ if (!this.authToken) throw new Error("Cannot start live transcription without an authenticated token");
1811
+ if (!audioTrack || audioTrack.kind !== "audio") {
1812
+ throw new Error("A live microphone audio track is required");
1813
+ }
1814
+ const mimeType = this.getLiveTranscriptionMimeType();
1815
+ this.liveTranscriptionActive = true;
1816
+ this.liveTranscriptionSequence = 0;
1817
+ this.liveTranscriptionStartedAt = Date.now();
1818
+ const stream = new MediaStream([audioTrack]);
1819
+ this.liveTranscriptionLoopPromise = this.runLiveTranscriptionLoop(
1820
+ stream,
1821
+ mimeType
1822
+ ).finally(() => {
1823
+ this.liveTranscriptionRecorder = null;
1824
+ this.liveTranscriptionLoopPromise = null;
1825
+ });
1826
+ void speakerName;
1827
+ await Promise.resolve();
1828
+ }
1829
+ async captureLiveChunk(stream, mimeType) {
1830
+ return new Promise((resolve, reject) => {
1831
+ const chunks = [];
1832
+ const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
1833
+ this.liveTranscriptionRecorder = recorder;
1834
+ const timeout = setTimeout(() => {
1835
+ if (recorder.state !== "inactive") recorder.stop();
1836
+ }, 4500);
1837
+ recorder.ondataavailable = (event) => {
1838
+ if (event.data.size > 0) chunks.push(event.data);
1839
+ };
1840
+ recorder.onerror = () => {
1841
+ clearTimeout(timeout);
1842
+ reject(new Error("Live transcription recorder failed"));
1843
+ };
1844
+ recorder.onstop = () => {
1845
+ clearTimeout(timeout);
1846
+ this.liveTranscriptionRecorder = null;
1847
+ resolve(
1848
+ chunks.length ? new Blob(chunks, { type: recorder.mimeType || mimeType || "audio/webm" }) : null
1849
+ );
1850
+ };
1851
+ recorder.start();
1852
+ });
1853
+ }
1854
+ async runLiveTranscriptionLoop(stream, mimeType) {
1855
+ while (this.liveTranscriptionActive && stream.getAudioTracks()[0]?.readyState === "live") {
1856
+ try {
1857
+ const blob = await this.captureLiveChunk(stream, mimeType);
1858
+ if (!this.liveTranscriptionActive) break;
1859
+ if (!blob || blob.size === 0) continue;
1860
+ const sequence = this.liveTranscriptionSequence++;
1861
+ const offsetMs = Math.max(0, Date.now() - this.liveTranscriptionStartedAt);
1862
+ await this.uploadLiveTranscriptionChunk(blob, sequence, offsetMs, mimeType);
1863
+ } catch (error) {
1864
+ this.onError(error);
1865
+ await new Promise((resolve) => setTimeout(resolve, 750));
1866
+ }
1867
+ }
1868
+ }
1869
+ async uploadLiveTranscriptionChunk(blob, sequence, offsetMs, mimeType) {
1870
+ if (!this.room || !this.authToken) return;
1871
+ const form = new FormData();
1872
+ form.append("sequence", String(sequence));
1873
+ form.append("offset_ms", String(offsetMs));
1874
+ const extension = mimeType.includes("ogg") || blob.type.includes("ogg") ? "ogg" : "webm";
1875
+ form.append("audio", blob, `live-${sequence}.${extension}`);
1876
+ const response = await fetch(
1877
+ `${this.apiBase}/rooms/${encodeURIComponent(this.room)}/live-transcription`,
1878
+ {
1879
+ method: "POST",
1880
+ headers: {
1881
+ Authorization: `Bearer ${this.authToken}`,
1882
+ Accept: "application/json"
1883
+ },
1884
+ body: form
1885
+ }
1886
+ );
1887
+ if (!response.ok) {
1888
+ let detail = `Live transcription request failed: ${response.status}`;
1889
+ try {
1890
+ const body = await response.json();
1891
+ detail = body?.message || body?.detail || detail;
1892
+ } catch {
1893
+ }
1894
+ throw new Error(detail);
1895
+ }
1896
+ }
1897
+ stopLiveTranscription() {
1898
+ this.liveTranscriptionActive = false;
1899
+ try {
1900
+ if (this.liveTranscriptionRecorder?.state !== "inactive") {
1901
+ this.liveTranscriptionRecorder?.stop();
1902
+ }
1903
+ } catch {
1904
+ }
1905
+ this.liveTranscriptionRecorder = null;
1906
+ }
1907
+ isLiveTranscriptionActive() {
1908
+ return this.liveTranscriptionActive;
1909
+ }
1910
+ async applyNetworkRecommendation(action) {
1911
+ if (!this.videoSender || this.networkProfile === action) return;
1912
+ const params = this.videoSender.getParameters();
1913
+ const encodings = params.encodings?.length ? params.encodings : [{}];
1914
+ const encoding = encodings[0];
1915
+ switch (action) {
1916
+ case "reduce_video":
1917
+ encoding.maxBitrate = 7e5;
1918
+ encoding.maxFramerate = 20;
1919
+ break;
1920
+ case "audio_priority":
1921
+ encoding.maxBitrate = 35e4;
1922
+ encoding.maxFramerate = 10;
1923
+ break;
1924
+ default:
1925
+ encoding.maxBitrate = 15e5;
1926
+ encoding.maxFramerate = 30;
1927
+ break;
1928
+ }
1929
+ params.encodings = encodings;
1930
+ try {
1931
+ await this.videoSender.setParameters(params);
1932
+ this.networkProfile = action;
1933
+ } catch (error) {
1934
+ this.onError(error);
1935
+ }
1936
+ }
1731
1937
  async startRecording() {
1732
1938
  if (!this.send({ type: "start_recording" })) {
1733
1939
  throw new Error("SFU signaling is not connected");
@@ -1860,6 +2066,7 @@ var SFUClient = class {
1860
2066
  }
1861
2067
  }
1862
2068
  async leave() {
2069
+ this.stopLiveTranscription();
1863
2070
  this.stopHeartbeat();
1864
2071
  this.intentionalClose = true;
1865
2072
  this.setState("leaving");
@@ -2268,6 +2475,14 @@ var VideoSDKCore = class {
2268
2475
  console.warn(`[SFU] Reconnecting, attempt ${attempt}`);
2269
2476
  },
2270
2477
  // ADMISSION CONTROL
2478
+ onLiveTranscript: (transcript) => {
2479
+ this.state.addLiveTranscript(transcript);
2480
+ this.events.onLiveTranscript?.(transcript);
2481
+ },
2482
+ onNetworkQuality: (quality) => {
2483
+ this.state.setNetworkQuality(quality);
2484
+ this.events.onNetworkQuality?.(quality);
2485
+ },
2271
2486
  onRoomUpdate: (payload) => {
2272
2487
  const participants = payload?.participants || [];
2273
2488
  const local = participants.find(
@@ -2912,6 +3127,28 @@ var VideoSDKCore = class {
2912
3127
  }
2913
3128
  }
2914
3129
  }
3130
+ async startLiveTranscription() {
3131
+ if (!this.sfuClient) {
3132
+ throw new Error("SFU client is not connected");
3133
+ }
3134
+ const audioTrack = this.localStream?.getAudioTracks()[0];
3135
+ if (!audioTrack) {
3136
+ throw new Error("Microphone audio track is not available");
3137
+ }
3138
+ await this.sfuClient.startLiveTranscription(
3139
+ audioTrack,
3140
+ this.participantName
3141
+ );
3142
+ }
3143
+ stopLiveTranscription() {
3144
+ this.sfuClient?.stopLiveTranscription();
3145
+ }
3146
+ isLiveTranscriptionActive() {
3147
+ return this.sfuClient?.isLiveTranscriptionActive() ?? false;
3148
+ }
3149
+ getNetworkQuality() {
3150
+ return this.state.networkQuality;
3151
+ }
2915
3152
  // CLEANUP
2916
3153
  cleanupRemoteParticipant(participantId) {
2917
3154
  this.activeSpeakerDetector.removeParticipant(participantId);
@@ -2960,6 +3197,8 @@ var VideoSDKCore = class {
2960
3197
  this.room.id = null;
2961
3198
  this.room.name = null;
2962
3199
  this.state.localParticipant = null;
3200
+ this.state.clearLiveTranscripts();
3201
+ this.state.setNetworkQuality(null);
2963
3202
  this.state.notify("localParticipant");
2964
3203
  this.state.resetRemoteState();
2965
3204
  this.events.onMeetingLeft?.();
@@ -2992,9 +3231,15 @@ var MeetingProvider = ({
2992
3231
  const entryResponseListeners = useRef(
2993
3232
  /* @__PURE__ */ new Set()
2994
3233
  );
2995
- const joinRequestCanceledListeners = useRef(/* @__PURE__ */ new Set());
2996
- const participantRemovedListeners = useRef(/* @__PURE__ */ new Set());
2997
- const participantPermissionsChangedListeners = useRef(/* @__PURE__ */ new Set());
3234
+ const joinRequestCanceledListeners = useRef(
3235
+ /* @__PURE__ */ new Set()
3236
+ );
3237
+ const participantRemovedListeners = useRef(
3238
+ /* @__PURE__ */ new Set()
3239
+ );
3240
+ const participantPermissionsChangedListeners = useRef(
3241
+ /* @__PURE__ */ new Set()
3242
+ );
2998
3243
  const meetingLeftListeners = useRef(/* @__PURE__ */ new Set());
2999
3244
  const trackStateListeners = useRef(/* @__PURE__ */ new Set());
3000
3245
  const recordingStartedListeners = useRef(/* @__PURE__ */ new Set());
@@ -3007,6 +3252,12 @@ var MeetingProvider = ({
3007
3252
  const activeSpeakerListeners = useRef(
3008
3253
  /* @__PURE__ */ new Set()
3009
3254
  );
3255
+ const liveTranscriptListeners = useRef(
3256
+ /* @__PURE__ */ new Set()
3257
+ );
3258
+ const networkQualityListeners = useRef(
3259
+ /* @__PURE__ */ new Set()
3260
+ );
3010
3261
  const connectionStateListeners = useRef(
3011
3262
  /* @__PURE__ */ new Set()
3012
3263
  );
@@ -3030,10 +3281,14 @@ var MeetingProvider = ({
3030
3281
  joinRequestCanceledListeners.current.forEach((fn) => fn(requestId));
3031
3282
  },
3032
3283
  onParticipantRemoved: (participantId, removedBy) => {
3033
- participantRemovedListeners.current.forEach((fn) => fn(participantId, removedBy));
3284
+ participantRemovedListeners.current.forEach(
3285
+ (fn) => fn(participantId, removedBy)
3286
+ );
3034
3287
  },
3035
3288
  onParticipantPermissionsChanged: (participantId, permissions) => {
3036
- participantPermissionsChangedListeners.current.forEach((fn) => fn(participantId, permissions));
3289
+ participantPermissionsChangedListeners.current.forEach(
3290
+ (fn) => fn(participantId, permissions)
3291
+ );
3037
3292
  },
3038
3293
  onMeetingLeft: () => {
3039
3294
  meetingLeftListeners.current.forEach((fn) => fn());
@@ -3086,6 +3341,12 @@ var MeetingProvider = ({
3086
3341
  );
3087
3342
  pubSubListeners.current.get("REACTION")?.forEach((fn) => fn(message));
3088
3343
  },
3344
+ onLiveTranscript: (transcript) => {
3345
+ liveTranscriptListeners.current.forEach((fn) => fn(transcript));
3346
+ },
3347
+ onNetworkQuality: (quality) => {
3348
+ networkQualityListeners.current.forEach((fn) => fn(quality));
3349
+ },
3089
3350
  onConnectionStateChanged: (state) => {
3090
3351
  connectionStateListeners.current.forEach(
3091
3352
  (fn) => fn(state)
@@ -3121,6 +3382,16 @@ var MeetingProvider = ({
3121
3382
  "chat",
3122
3383
  (s) => s.getChatMessages()
3123
3384
  );
3385
+ const liveTranscripts = useMeetingStore(
3386
+ sdk.state,
3387
+ "transcript",
3388
+ (s) => s.getLiveTranscripts()
3389
+ );
3390
+ const networkQuality = useMeetingStore(
3391
+ sdk.state,
3392
+ "networkQuality",
3393
+ (s) => s.networkQuality
3394
+ );
3124
3395
  const value = useMemo(() => {
3125
3396
  return {
3126
3397
  sdk,
@@ -3145,6 +3416,11 @@ var MeetingProvider = ({
3145
3416
  messages,
3146
3417
  presenterId,
3147
3418
  activeSpeakerIds,
3419
+ liveTranscripts,
3420
+ networkQuality,
3421
+ startLiveTranscription: sdk.startLiveTranscription.bind(sdk),
3422
+ stopLiveTranscription: sdk.stopLiveTranscription.bind(sdk),
3423
+ isLiveTranscriptionActive: sdk.isLiveTranscriptionActive.bind(sdk),
3148
3424
  recording,
3149
3425
  usePubSub: (topic) => {
3150
3426
  if (topic === "SECURE_CHAT") {
@@ -3263,6 +3539,14 @@ var MeetingProvider = ({
3263
3539
  reactionListeners.current.add(cb);
3264
3540
  return () => reactionListeners.current.delete(cb);
3265
3541
  },
3542
+ onLiveTranscript: (cb) => {
3543
+ liveTranscriptListeners.current.add(cb);
3544
+ return () => liveTranscriptListeners.current.delete(cb);
3545
+ },
3546
+ onNetworkQuality: (cb) => {
3547
+ networkQualityListeners.current.add(cb);
3548
+ return () => networkQualityListeners.current.delete(cb);
3549
+ },
3266
3550
  onConnectionStateChanged: (cb) => {
3267
3551
  connectionStateListeners.current.add(cb);
3268
3552
  return () => {
@@ -3285,6 +3569,8 @@ var MeetingProvider = ({
3285
3569
  presenterId,
3286
3570
  recording,
3287
3571
  activeSpeakerIds,
3572
+ liveTranscripts,
3573
+ networkQuality,
3288
3574
  reactionPubSubMessages
3289
3575
  ]);
3290
3576
  return /* @__PURE__ */ jsx(MeetingContext.Provider, { value, children });
@@ -3369,7 +3655,9 @@ var useMeeting = (handlers) => {
3369
3655
  }, [handlers?.onParticipantRemoved]);
3370
3656
  useEffect3(() => {
3371
3657
  if (!handlers?.onParticipantPermissionsChanged) return;
3372
- return ctx.onParticipantPermissionsChanged(handlers.onParticipantPermissionsChanged);
3658
+ return ctx.onParticipantPermissionsChanged(
3659
+ handlers.onParticipantPermissionsChanged
3660
+ );
3373
3661
  }, [handlers?.onParticipantPermissionsChanged]);
3374
3662
  useEffect3(() => {
3375
3663
  if (!handlers?.onMeetingLeft) return;
@@ -3391,6 +3679,14 @@ var useMeeting = (handlers) => {
3391
3679
  if (!handlers?.onConnectionStateChanged) return;
3392
3680
  return ctx.onConnectionStateChanged(handlers.onConnectionStateChanged);
3393
3681
  }, [handlers?.onConnectionStateChanged]);
3682
+ useEffect3(() => {
3683
+ if (!handlers?.onLiveTranscript) return;
3684
+ return ctx.onLiveTranscript(handlers.onLiveTranscript);
3685
+ }, [handlers?.onLiveTranscript]);
3686
+ useEffect3(() => {
3687
+ if (!handlers?.onNetworkQuality) return;
3688
+ return ctx.onNetworkQuality(handlers.onNetworkQuality);
3689
+ }, [handlers?.onNetworkQuality]);
3394
3690
  useEffect3(() => {
3395
3691
  if (!handlers?.onReaction) return;
3396
3692
  return ctx.onReaction(handlers.onReaction);
@@ -3528,6 +3824,26 @@ function useMeetingPreview(roomId, participantId) {
3528
3824
  };
3529
3825
  }
3530
3826
 
3827
+ // src/react/useLiveTranscript.ts
3828
+ var useLiveTranscript = () => {
3829
+ const { sdk } = useMeetingContext();
3830
+ return useMeetingStore(
3831
+ sdk.state,
3832
+ "transcript",
3833
+ (state) => state.getLiveTranscripts()
3834
+ );
3835
+ };
3836
+
3837
+ // src/react/useNetworkQuality.ts
3838
+ var useNetworkQuality = () => {
3839
+ const { sdk } = useMeetingContext();
3840
+ return useMeetingStore(
3841
+ sdk.state,
3842
+ "networkQuality",
3843
+ (state) => state.networkQuality
3844
+ );
3845
+ };
3846
+
3531
3847
  // src/react/usePubSub.ts
3532
3848
  import { useEffect as useEffect7 } from "react";
3533
3849
  function usePubSub(topic, options) {
@@ -3546,10 +3862,12 @@ export {
3546
3862
  MeetingProvider,
3547
3863
  MeetingState,
3548
3864
  VideoSDKCore,
3865
+ useLiveTranscript,
3549
3866
  useLocalParticipant,
3550
3867
  useMeeting,
3551
3868
  useMeetingContext,
3552
3869
  useMeetingPreview,
3870
+ useNetworkQuality,
3553
3871
  useParticipants,
3554
3872
  usePubSub,
3555
3873
  useRemoteMedia