@fishjam-cloud/react-client 0.25.2 → 0.26.0-rc.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"useDeviceManager.d.ts","sourceRoot":"","sources":["../../../../src/hooks/internal/devices/useDeviceManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAG5C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAMtF,KAAK,kBAAkB,GAAG;IACxB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC;IACrE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC;IACrE,gBAAgB,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACpD,UAAU,EAAE,OAAO,GAAG,OAAO,CAAC;IAC9B,cAAc,EAAE,eAAe,EAAE,CAAC;IAClC,WAAW,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC;IAC9C,iBAAiB,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IACrD,cAAc,EAAE,eAAe,GAAG,IAAI,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC;IACnG,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,GAAG,SAAS,CAAC;IACxG,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,WAAW,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACrC,UAAU,EAAE,UAAU,EAAE,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,iBAAiB,EAAE,eAAe,CAAC;IACnC,eAAe,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACnF,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,eAAe,GAAG,IAAI,CAAC;CACxC,CAAC;AAsCF,eAAO,MAAM,gBAAgB,GAAI,qKAY9B,kBAAkB,KAAG,aAiIvB,CAAC"}
1
+ {"version":3,"file":"useDeviceManager.d.ts","sourceRoot":"","sources":["../../../../src/hooks/internal/devices/useDeviceManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAG5C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAMtF,KAAK,kBAAkB,GAAG;IACxB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC;IACrE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC;IACrE,gBAAgB,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACpD,UAAU,EAAE,OAAO,GAAG,OAAO,CAAC;IAC9B,cAAc,EAAE,eAAe,EAAE,CAAC;IAClC,WAAW,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC;IAC9C,iBAAiB,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IACrD,cAAc,EAAE,eAAe,GAAG,IAAI,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC;IACnG,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,GAAG,SAAS,CAAC;IACxG,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,WAAW,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACrC,UAAU,EAAE,UAAU,EAAE,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,iBAAiB,EAAE,eAAe,CAAC;IACnC,eAAe,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACnF,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,eAAe,GAAG,IAAI,CAAC;CACxC,CAAC;AAiBF,eAAO,MAAM,gBAAgB,GAAI,qKAY9B,kBAAkB,KAAG,aA0IvB,CAAC"}
@@ -1,24 +1,8 @@
1
- import { useCallback, useMemo, useState } from "react";
1
+ import { useCallback, useMemo, useRef, useState } from "react";
2
2
  import { parseUserMediaError } from "../../../utils/errors";
3
3
  import { getTrackFromStream, stopStream } from "../../../utils/track";
4
4
  import { useTrackMiddleware } from "../useTrackMiddleware";
5
5
  import { useHandleTrackEnd } from "./useHandleTrackEnd";
6
- function getReplaceStreamAction(newStream, deviceType) {
7
- return (oldStream) => {
8
- if (oldStream) {
9
- stopStream(oldStream, deviceType);
10
- }
11
- return newStream;
12
- };
13
- }
14
- function getStopStreamAction(deviceType) {
15
- return (oldStream) => {
16
- if (oldStream) {
17
- stopStream(oldStream, deviceType);
18
- }
19
- return null;
20
- };
21
- }
22
6
  async function getDeviceStream(type, constraints, deviceId) {
23
7
  constraints = typeof constraints === "object" ? constraints : {};
24
8
  if (deviceId) {
@@ -30,6 +14,8 @@ async function getDeviceStream(type, constraints, deviceId) {
30
14
  return stream;
31
15
  }
32
16
  export const useDeviceManager = ({ mediaStream, setMediaStream, getInitialStream, deviceType, constraints, allDevicesList, setSelectedDevice, deviceError, setDeviceError, selectedDevice, logger, }) => {
17
+ const mediaStreamRef = useRef(mediaStream);
18
+ mediaStreamRef.current = mediaStream;
33
19
  const rawTrack = useMemo(() => mediaStream && getTrackFromStream(mediaStream, deviceType), [mediaStream, deviceType]);
34
20
  const clearStream = useCallback(() => {
35
21
  setMediaStream(null);
@@ -61,7 +47,10 @@ export const useDeviceManager = ({ mediaStream, setMediaStream, getInitialStream
61
47
  }
62
48
  try {
63
49
  const stream = await getDeviceStream(deviceType, constraints, deviceId ?? null);
64
- setMediaStream(getReplaceStreamAction(stream, deviceType));
50
+ if (mediaStreamRef.current) {
51
+ stopStream(mediaStreamRef.current, deviceType);
52
+ }
53
+ setMediaStream(stream);
65
54
  const retrievedTrack = stream && getTrackFromStream(stream, deviceType);
66
55
  const retrievedTrackDeviceId = retrievedTrack.getSettings().deviceId;
67
56
  if (retrievedTrackDeviceId) {
@@ -97,7 +86,10 @@ export const useDeviceManager = ({ mediaStream, setMediaStream, getInitialStream
97
86
  }
98
87
  }, [currentTrack, setSelectedDeviceId, startDevice]);
99
88
  const stopDevice = useCallback(() => {
100
- setMediaStream(getStopStreamAction(deviceType));
89
+ if (mediaStreamRef.current) {
90
+ stopStream(mediaStreamRef.current, deviceType);
91
+ }
92
+ setMediaStream(null);
101
93
  }, [setMediaStream, deviceType]);
102
94
  const enableDevice = useCallback(() => {
103
95
  if (!currentTrack)
@@ -1 +1 @@
1
- {"version":3,"file":"useMediaDevices.d.ts","sourceRoot":"","sources":["../../../../src/hooks/internal/devices/useMediaDevices.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAKvD,OAAO,KAAK,EAAe,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAG7G,UAAU,eAAe;IACvB,gBAAgB,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC;IACnD,gBAAgB,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC;IACnD,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,yBAAyB,GAAG;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEzF,eAAO,MAAM,eAAe,GAAI,iEAAiE,eAAe;mCAmC1F,yBAAyB,KAAG,OAAO,CAAC,uBAAuB,CAAC;;;CA2GjF,CAAC"}
1
+ {"version":3,"file":"useMediaDevices.d.ts","sourceRoot":"","sources":["../../../../src/hooks/internal/devices/useMediaDevices.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAKvD,OAAO,KAAK,EAAe,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAG7G,UAAU,eAAe;IACvB,gBAAgB,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC;IACnD,gBAAgB,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC;IACnD,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,yBAAyB,GAAG;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEzF,eAAO,MAAM,eAAe,GAAI,iEAAiE,eAAe;mCAoC1F,yBAAyB,KAAG,OAAO,CAAC,uBAAuB,CAAC;;;CAwHjF,CAAC"}
@@ -10,6 +10,7 @@ export const useMediaDevices = ({ videoConstraints, audioConstraints, persistHan
10
10
  const [audioError, setAudioError] = useState(null);
11
11
  const [selectedCamera, setSelectedCamera] = useState(persistHandlers?.getLastDevice("video") ?? null);
12
12
  const [selectedMic, setSelectedMic] = useState(persistHandlers?.getLastDevice("audio") ?? null);
13
+ const isInitializedRef = useRef(false);
13
14
  const initializationRef = useRef(null);
14
15
  const selectCamera = useCallback((deviceInfo) => {
15
16
  setSelectedCamera(deviceInfo);
@@ -20,12 +21,15 @@ export const useMediaDevices = ({ videoConstraints, audioConstraints, persistHan
20
21
  persistHandlers?.saveLastDevice(deviceInfo, "audio");
21
22
  }, [persistHandlers]);
22
23
  const initializeDevices = useCallback(async (settings) => {
23
- if (deviceList.length) {
24
+ if (isInitializedRef.current) {
24
25
  return { stream: null, errors: null, status: "already_initialized" };
25
26
  }
27
+ if (initializationRef.current) {
28
+ return initializationRef.current;
29
+ }
26
30
  const lastUsed = {
27
- audio: selectedMic,
28
- video: selectedCamera,
31
+ audio: persistHandlers?.getLastDevice("audio") ?? null,
32
+ video: persistHandlers?.getLastDevice("video") ?? null,
29
33
  };
30
34
  const constraints = {
31
35
  video: settings?.enableVideo !== false && prepareConstraints(lastUsed.video?.deviceId, videoConstraints),
@@ -63,10 +67,16 @@ export const useMediaDevices = ({ videoConstraints, audioConstraints, persistHan
63
67
  return { status: "initialized", errors: null, stream };
64
68
  }
65
69
  };
66
- const initializePromise = intitialize();
70
+ const initializePromise = intitialize().then((result) => {
71
+ isInitializedRef.current = true;
72
+ return result;
73
+ }, (error) => {
74
+ initializationRef.current = null;
75
+ throw error;
76
+ });
67
77
  initializationRef.current = initializePromise;
68
78
  return await initializePromise;
69
- }, [deviceList, selectedMic, selectedCamera, videoConstraints, audioConstraints, selectCamera, selectMic]);
79
+ }, [videoConstraints, audioConstraints, selectCamera, selectMic, persistHandlers]);
70
80
  useEffect(() => {
71
81
  const isInitialStreamIrrelevant = videoStream !== audioStream;
72
82
  if (isInitialStreamIrrelevant) {
@@ -0,0 +1,20 @@
1
+ import type { PeerId } from "../types/public";
2
+ /**
3
+ * Client-side voice activity detection for the local peer.
4
+ *
5
+ * Polls the local microphone's audio level every 100ms and derives a speech/silence
6
+ * state from it. A level above ~0.025 (approximately −32 dBov, scaled to [0, 1])
7
+ * is treated as speech. Silence is debounced over 2 consecutive ticks (~200ms)
8
+ * to prevent rapid flapping.
9
+ *
10
+ * This is purely client-side — it does not signal other peers. Remote participants
11
+ * receive the local peer's VAD status via backend `vadNotification` messages.
12
+ *
13
+ * @internal Used by `useVAD` when the local peer's id is included in `peerIds`.
14
+ * @returns A record mapping the local peer's id to its current speaking state,
15
+ * or an empty object if `options.disabled` is true, the local peer is not available, or no microphone track is found.
16
+ */
17
+ export declare const useLocalVAD: (options: {
18
+ disabled: boolean;
19
+ }) => Record<PeerId, boolean>;
20
+ //# sourceMappingURL=useLocalVAD.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLocalVAD.d.ts","sourceRoot":"","sources":["../../src/hooks/useLocalVAD.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAW9C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAsClF,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { useContext, useEffect, useState } from "react";
2
+ import { FishjamClientContext } from "../contexts/fishjamClient";
3
+ import { usePeers } from "./usePeers";
4
+ // This is a dBov-to-linear conversion. -32 dBov number is taken from backend VAD threshold
5
+ // formula for dBov to linear conversion: linear = 10 ^ (dBov / 20)
6
+ // So -32 dBov = 10^(-32/20) ≈ 0.025. This is the minimum audio level considered "speech".
7
+ const THRESHOLD = 10 ** (-32 / 20);
8
+ // Number of consecutive "silence" ticks before we consider speech to have stopped. Helps with smoothing out brief pauses in speech.
9
+ const SILENCE_DEBOUNCE_TICKS = 2;
10
+ /**
11
+ * Client-side voice activity detection for the local peer.
12
+ *
13
+ * Polls the local microphone's audio level every 100ms and derives a speech/silence
14
+ * state from it. A level above ~0.025 (approximately −32 dBov, scaled to [0, 1])
15
+ * is treated as speech. Silence is debounced over 2 consecutive ticks (~200ms)
16
+ * to prevent rapid flapping.
17
+ *
18
+ * This is purely client-side — it does not signal other peers. Remote participants
19
+ * receive the local peer's VAD status via backend `vadNotification` messages.
20
+ *
21
+ * @internal Used by `useVAD` when the local peer's id is included in `peerIds`.
22
+ * @returns A record mapping the local peer's id to its current speaking state,
23
+ * or an empty object if `options.disabled` is true, the local peer is not available, or no microphone track is found.
24
+ */
25
+ export const useLocalVAD = (options) => {
26
+ const fishjamClient = useContext(FishjamClientContext);
27
+ const [isSpeaking, setIsSpeaking] = useState(false);
28
+ const { localPeer } = usePeers();
29
+ const localPeerId = localPeer?.id;
30
+ const microphoneTrackId = localPeer?.microphoneTrack?.trackId;
31
+ useEffect(() => {
32
+ if (options.disabled || !localPeerId || !microphoneTrackId)
33
+ return;
34
+ let silenceTicks = 0;
35
+ let timeoutId;
36
+ const poll = async () => {
37
+ const trackAudio = await fishjamClient?.current?.getLocalTrackAudioLevel(microphoneTrackId);
38
+ if (trackAudio != null && trackAudio.level > THRESHOLD) {
39
+ silenceTicks = 0;
40
+ setIsSpeaking(true);
41
+ }
42
+ else {
43
+ silenceTicks += 1;
44
+ if (silenceTicks >= SILENCE_DEBOUNCE_TICKS) {
45
+ setIsSpeaking(false);
46
+ }
47
+ }
48
+ timeoutId = setTimeout(poll, 100);
49
+ };
50
+ timeoutId = setTimeout(poll, 0);
51
+ return () => {
52
+ clearTimeout(timeoutId);
53
+ setIsSpeaking(false);
54
+ };
55
+ }, [options.disabled, fishjamClient, localPeerId, microphoneTrackId]);
56
+ if (!localPeerId || options.disabled || !microphoneTrackId)
57
+ return {};
58
+ return { [localPeerId]: isSpeaking };
59
+ };
@@ -1,14 +1,21 @@
1
1
  import type { PeerId } from "../types/public";
2
2
  /**
3
- * Voice activity detection. Use this hook to check if voice is detected in audio track for given peer(s).
3
+ * Voice activity detection. Use this hook to check if voice is detected in the audio track for given peer(s).
4
4
  *
5
- * @param options - Options object containing `peerIds` - a list of ids of peers to subscribe to for voice activity detection notifications.
5
+ * Remote peer VAD is driven by `vadNotification` messages from the backend.
6
+ * If the local peer's id is included in `peerIds`, local VAD is determined client-side
7
+ * by polling the microphone's audio level (see `useLocalVAD`).
8
+ *
9
+ * @param options - Options object.
10
+ * @param options.peerIds - List of peer ids to subscribe to for VAD notifications.
11
+ * Include the local peer's id to also track whether the local user is speaking.
6
12
  *
7
13
  * Example usage:
8
14
  * ```tsx
9
15
  * import { useVAD, type PeerId } from "@fishjam-cloud/react-client";
16
+ *
10
17
  * function WhoIsTalkingComponent({ peerIds }: { peerIds: PeerId[] }) {
11
- * const peersInfo = useVAD({peerIds});
18
+ * const peersInfo = useVAD({ peerIds });
12
19
  * const activePeers = (Object.keys(peersInfo) as PeerId[]).filter((peerId) => peersInfo[peerId]);
13
20
  *
14
21
  * return "Now talking: " + activePeers.join(", ");
@@ -16,7 +23,8 @@ import type { PeerId } from "../types/public";
16
23
  * ```
17
24
  * @category Connection
18
25
  * @group Hooks
19
- * @returns Each key is a peerId and the boolean value indicates if voice activity is currently detected for that peer.
26
+ * @returns A record where each key is a peer id and the boolean value indicates
27
+ * whether voice activity is currently detected for that peer.
20
28
  */
21
29
  export declare const useVAD: (options: {
22
30
  peerIds: ReadonlyArray<PeerId>;
@@ -1 +1 @@
1
- {"version":3,"file":"useVAD.d.ts","sourceRoot":"","sources":["../../src/hooks/useVAD.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,MAAM,GAAI,SAAS;IAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAAE,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAiE1F,CAAC"}
1
+ {"version":3,"file":"useVAD.d.ts","sourceRoot":"","sources":["../../src/hooks/useVAD.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AAGvD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,MAAM,GAAI,SAAS;IAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAAE,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAuE1F,CAAC"}
@@ -1,15 +1,23 @@
1
1
  import { useContext, useEffect, useMemo, useState } from "react";
2
2
  import { FishjamClientStateContext } from "../contexts/fishjamState";
3
+ import { useLocalVAD } from "./useLocalVAD";
3
4
  /**
4
- * Voice activity detection. Use this hook to check if voice is detected in audio track for given peer(s).
5
+ * Voice activity detection. Use this hook to check if voice is detected in the audio track for given peer(s).
5
6
  *
6
- * @param options - Options object containing `peerIds` - a list of ids of peers to subscribe to for voice activity detection notifications.
7
+ * Remote peer VAD is driven by `vadNotification` messages from the backend.
8
+ * If the local peer's id is included in `peerIds`, local VAD is determined client-side
9
+ * by polling the microphone's audio level (see `useLocalVAD`).
10
+ *
11
+ * @param options - Options object.
12
+ * @param options.peerIds - List of peer ids to subscribe to for VAD notifications.
13
+ * Include the local peer's id to also track whether the local user is speaking.
7
14
  *
8
15
  * Example usage:
9
16
  * ```tsx
10
17
  * import { useVAD, type PeerId } from "@fishjam-cloud/react-client";
18
+ *
11
19
  * function WhoIsTalkingComponent({ peerIds }: { peerIds: PeerId[] }) {
12
- * const peersInfo = useVAD({peerIds});
20
+ * const peersInfo = useVAD({ peerIds });
13
21
  * const activePeers = (Object.keys(peersInfo) as PeerId[]).filter((peerId) => peersInfo[peerId]);
14
22
  *
15
23
  * return "Now talking: " + activePeers.join(", ");
@@ -17,46 +25,52 @@ import { FishjamClientStateContext } from "../contexts/fishjamState";
17
25
  * ```
18
26
  * @category Connection
19
27
  * @group Hooks
20
- * @returns Each key is a peerId and the boolean value indicates if voice activity is currently detected for that peer.
28
+ * @returns A record where each key is a peer id and the boolean value indicates
29
+ * whether voice activity is currently detected for that peer.
21
30
  */
22
31
  export const useVAD = (options) => {
23
32
  const { peerIds } = options;
24
33
  const clientState = useContext(FishjamClientStateContext);
25
34
  if (!clientState)
26
35
  throw Error("useVAD must be used within FishjamProvider");
36
+ const showLocalPeerVAD = useMemo(() => (clientState.localPeer?.id ? peerIds.includes(clientState.localPeer?.id) : false), [clientState.localPeer?.id, peerIds]);
27
37
  const micTracksWithSelectedPeerIds = useMemo(() => Object.values(clientState.peers)
28
38
  .filter((peer) => peerIds.includes(peer.id))
29
39
  .map((peer) => ({
30
40
  peerId: peer.id,
31
- microphoneTracks: Array.from(peer.tracks.values()).filter(({ metadata }) => metadata?.type === "microphone"),
41
+ microphoneTrack: Array.from(peer.tracks.values()).find(({ metadata }) => metadata?.type === "microphone"),
32
42
  })), [clientState.peers, peerIds]);
33
- const getDefaultVadStatuses = () => micTracksWithSelectedPeerIds.reduce((mappedTracks, peer) => ({
43
+ const getDefaultVadStatuses = () => micTracksWithSelectedPeerIds.reduce((mappedTracks, { peerId, microphoneTrack }) => ({
34
44
  ...mappedTracks,
35
- [peer.peerId]: peer.microphoneTracks.reduce((vadStatuses, track) => ({ ...vadStatuses, [track.trackId]: track.vadStatus }), {}),
45
+ [peerId]: microphoneTrack ? { [microphoneTrack.trackId]: microphoneTrack.vadStatus } : {},
36
46
  }), {});
37
47
  const [_vadStatuses, setVadStatuses] = useState(getDefaultVadStatuses);
38
48
  useEffect(() => {
39
- const unsubs = micTracksWithSelectedPeerIds.map(({ peerId, microphoneTracks }) => {
49
+ const unsubs = micTracksWithSelectedPeerIds.map(({ peerId, microphoneTrack }) => {
40
50
  const updateVadStatus = (track) => {
41
51
  setVadStatuses((prev) => ({
42
52
  ...prev,
43
53
  [peerId]: { ...prev[peerId], [track.trackId]: track.vadStatus },
44
54
  }));
45
55
  };
46
- microphoneTracks.forEach((track) => {
47
- track.on("voiceActivityChanged", updateVadStatus);
48
- });
56
+ if (microphoneTrack) {
57
+ microphoneTrack.on("voiceActivityChanged", updateVadStatus);
58
+ }
49
59
  return () => {
50
- microphoneTracks.forEach((track) => {
51
- track.off("voiceActivityChanged", updateVadStatus);
52
- });
60
+ if (microphoneTrack) {
61
+ microphoneTrack.off("voiceActivityChanged", updateVadStatus);
62
+ }
53
63
  };
54
64
  });
55
65
  return () => unsubs.forEach((unsub) => unsub());
56
66
  }, [micTracksWithSelectedPeerIds]);
57
- const vadStatuses = useMemo(() => Object.fromEntries(Object.entries(_vadStatuses).map(([peerId, tracks]) => [
58
- peerId,
59
- Object.values(tracks).some((vad) => vad === "speech"),
60
- ])), [_vadStatuses]);
67
+ const localVAD = useLocalVAD({ disabled: !showLocalPeerVAD });
68
+ const vadStatuses = useMemo(() => ({
69
+ ...Object.fromEntries(Object.entries(_vadStatuses).map(([peerId, tracks]) => [
70
+ peerId,
71
+ Object.values(tracks).some((vad) => vad === "speech"),
72
+ ])),
73
+ ...localVAD,
74
+ }), [_vadStatuses, localVAD]);
61
75
  return vadStatuses;
62
76
  };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * React client SDK for building web video and audio apps with Fishjam.
3
+ *
4
+ * @packageDocumentation
5
+ */
1
6
  export { FishjamProvider, type FishjamProviderProps } from "./FishjamProvider";
2
7
  export { useCamera } from "./hooks/devices/useCamera";
3
8
  export { useInitializeDevices, UseInitializeDevicesParams } from "./hooks/devices/useInitializeDevices";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AACxG,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,OAAO,EAAE,KAAK,cAAc,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,qBAAqB,EACrB,KAAK,2BAA2B,GACjC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,KAAK,mBAAmB,EACxB,mBAAmB,EACnB,KAAK,yBAAyB,GAC/B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,KAAK,cAAc,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,QAAQ,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,YAAY,EACV,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,UAAU,EACV,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,MAAM,EACN,UAAU,EACV,yBAAyB,EACzB,wBAAwB,EACxB,YAAY,EACZ,KAAK,EACL,OAAO,EACP,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,QAAQ,EACR,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,eAAe,EACf,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AACxG,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,OAAO,EAAE,KAAK,cAAc,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,qBAAqB,EACrB,KAAK,2BAA2B,GACjC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,KAAK,mBAAmB,EACxB,mBAAmB,EACnB,KAAK,yBAAyB,GAC/B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,KAAK,cAAc,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,QAAQ,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,YAAY,EACV,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,UAAU,EACV,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,MAAM,EACN,UAAU,EACV,yBAAyB,EACzB,wBAAwB,EACxB,YAAY,EACZ,KAAK,EACL,OAAO,EACP,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,QAAQ,EACR,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,eAAe,EACf,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * React client SDK for building web video and audio apps with Fishjam.
3
+ *
4
+ * @packageDocumentation
5
+ */
1
6
  export { FishjamProvider } from "./FishjamProvider";
2
7
  export { useCamera } from "./hooks/devices/useCamera";
3
8
  export { useInitializeDevices } from "./hooks/devices/useInitializeDevices";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishjam-cloud/react-client",
3
- "version": "0.25.2",
3
+ "version": "0.26.0-rc.0",
4
4
  "description": "React client library for Fishjam",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Fishjam Team",
@@ -62,7 +62,7 @@
62
62
  "vitest": "^3.1.1"
63
63
  },
64
64
  "dependencies": {
65
- "@fishjam-cloud/ts-client": "workspace:*",
65
+ "@fishjam-cloud/ts-client": "0.26.0-rc.0",
66
66
  "events": "3.3.0",
67
67
  "lodash.isequal": "4.5.0"
68
68
  },
@@ -78,4 +78,4 @@
78
78
  "yarn lint"
79
79
  ]
80
80
  }
81
- }
81
+ }