@fishjam-cloud/react-client 0.26.0 → 0.26.1

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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Returns a stable function reference whose body always reads the latest
3
+ * closure values. Use when a method needs to be called from effects or
4
+ * captured by consumers without being memoized against changing state
5
+ * (peerStatus, deviceTrack, etc.) in its dep array.
6
+ */
7
+ export declare const useCurrentCallback: <Args extends unknown[], R>(handler: (...args: Args) => R) => (...args: Args) => R;
8
+ //# sourceMappingURL=useCurrentCallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCurrentCallback.d.ts","sourceRoot":"","sources":["../../../src/hooks/internal/useCurrentCallback.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC,EAAE,SAAS,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,eAK5D,IAAI,MAClC,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { useCallback, useLayoutEffect, useRef } from "react";
2
+ /**
3
+ * Returns a stable function reference whose body always reads the latest
4
+ * closure values. Use when a method needs to be called from effects or
5
+ * captured by consumers without being memoized against changing state
6
+ * (peerStatus, deviceTrack, etc.) in its dep array.
7
+ */
8
+ export const useCurrentCallback = (handler) => {
9
+ const ref = useRef(handler);
10
+ useLayoutEffect(() => {
11
+ ref.current = handler;
12
+ });
13
+ return useCallback((...args) => ref.current(...args), []);
14
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"useScreenshareManager.d.ts","sourceRoot":"","sources":["../../../src/hooks/internal/useScreenshareManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAAsC,MAAM,0BAA0B,CAAC;AAI/G,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,cAAc,EAAE,CAAC,KAAK,CAAC,EAAE;QACvB,gBAAgB,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC;QACnD,gBAAgB,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC;KACpD,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB;;OAEG;IACH,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B;;OAEG;IACH,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpC;;OAEG;IACH,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpC;;;;OAIG;IACH,uBAAuB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjD;;;;OAIG;IACH,mBAAmB,EAAE,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7E,CAAC;AAEF,UAAU,uBAAuB;IAC/B,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,qBAAqB,GAAI,wCAInC,uBAAuB,KAAG,oBAiL5B,CAAC"}
1
+ {"version":3,"file":"useScreenshareManager.d.ts","sourceRoot":"","sources":["../../../src/hooks/internal/useScreenshareManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAAsC,MAAM,0BAA0B,CAAC;AAI/G,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGvE,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,cAAc,EAAE,CAAC,KAAK,CAAC,EAAE;QACvB,gBAAgB,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC;QACnD,gBAAgB,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC;KACpD,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB;;OAEG;IACH,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B;;OAEG;IACH,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpC;;OAEG;IACH,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpC;;;;OAIG;IACH,uBAAuB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjD;;;;OAIG;IACH,mBAAmB,EAAE,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7E,CAAC;AAEF,UAAU,uBAAuB;IAC/B,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,qBAAqB,GAAI,wCAInC,uBAAuB,KAAG,oBA8K5B,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { TrackTypeError } from "@fishjam-cloud/ts-client";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useCurrentCallback } from "./useCurrentCallback";
3
4
  export const useScreenShareManager = ({ fishjamClient, peerStatus, logger, }) => {
4
5
  const [state, setState] = useState({ stream: null, trackIds: null });
5
6
  const cleanMiddlewareFnRef = useRef(null);
@@ -79,7 +80,9 @@ export const useScreenShareManager = ({ fishjamClient, peerStatus, logger, }) =>
79
80
  cleanMiddlewareFnRef.current = onClear;
80
81
  await replaceTracks(videoTrack, audioTrack);
81
82
  }, [state.stream, cleanMiddleware, replaceTracks]);
82
- const stopStreaming = useCallback(async () => {
83
+ // Stable identity with a live closure: peerStatus must be observed at call
84
+ // time so a captured reference doesn't skip the SFU removeTrack calls.
85
+ const stopStreaming = useCurrentCallback(async () => {
83
86
  if (!state.stream) {
84
87
  logger.warn("No stream to stop");
85
88
  return;
@@ -98,21 +101,15 @@ export const useScreenShareManager = ({ fishjamClient, peerStatus, logger, }) =>
98
101
  }
99
102
  cleanMiddleware();
100
103
  setState((prev) => ({ stream: null, trackIds: null, tracksMiddleware: prev.tracksMiddleware }));
101
- }, [
102
- state.stream,
103
- state.trackIds?.videoId,
104
- state.trackIds?.audioId,
105
- peerStatus,
106
- cleanMiddleware,
107
- logger,
108
- fishjamClient,
109
- ]);
104
+ });
110
105
  useEffect(() => {
111
106
  if (!state.stream)
112
107
  return;
113
108
  const [video, audio] = getTracksFromStream(state.stream);
114
109
  const trackEndedHandler = () => {
115
- stopStreaming();
110
+ void stopStreaming().catch((err) => {
111
+ logger.error(err);
112
+ });
116
113
  };
117
114
  video.addEventListener("ended", trackEndedHandler);
118
115
  audio?.addEventListener("ended", trackEndedHandler);
@@ -120,18 +117,20 @@ export const useScreenShareManager = ({ fishjamClient, peerStatus, logger, }) =>
120
117
  video.removeEventListener("ended", trackEndedHandler);
121
118
  audio?.removeEventListener("ended", trackEndedHandler);
122
119
  };
123
- }, [state, stopStreaming]);
120
+ }, [state, stopStreaming, logger]);
124
121
  useEffect(() => {
125
122
  const onDisconnected = () => {
126
- if (stream) {
127
- stopStreaming();
128
- }
123
+ if (!stream)
124
+ return;
125
+ void stopStreaming().catch((err) => {
126
+ logger.error(err);
127
+ });
129
128
  };
130
129
  fishjamClient.on("disconnected", onDisconnected);
131
130
  return () => {
132
131
  fishjamClient.removeListener("disconnected", onDisconnected);
133
132
  };
134
- }, [stopStreaming, fishjamClient, stream]);
133
+ }, [stopStreaming, fishjamClient, stream, logger]);
135
134
  return {
136
135
  startStreaming,
137
136
  stopStreaming,
@@ -1 +1 @@
1
- {"version":3,"file":"useTrackManager.d.ts","sourceRoot":"","sources":["../../../src/hooks/internal/useTrackManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAA+C,MAAM,0BAA0B,CAAC;AAGxH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAmB,MAAM,oBAAoB,CAAC;AAErG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,UAAU,kBAAkB;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,aAAa,CAAC;IACxB,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,eAAe,CAAC;IACjC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,IAAI,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,eAAe,GAAI,uFAQ7B,kBAAkB,KAAG,YAoLvB,CAAC"}
1
+ {"version":3,"file":"useTrackManager.d.ts","sourceRoot":"","sources":["../../../src/hooks/internal/useTrackManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAA+C,MAAM,0BAA0B,CAAC;AAGxH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAmB,MAAM,oBAAoB,CAAC;AAErG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGhE,UAAU,kBAAkB;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,aAAa,CAAC;IACxB,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,eAAe,CAAC;IACjC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,IAAI,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,eAAe,GAAI,uFAQ7B,kBAAkB,KAAG,YA2KvB,CAAC"}
@@ -1,36 +1,44 @@
1
1
  import { TrackTypeError, Variant } from "@fishjam-cloud/ts-client";
2
- import { useCallback, useEffect, useRef } from "react";
2
+ import { useEffect, useRef } from "react";
3
3
  import { getConfigAndBandwidthFromProps, getRemoteOrLocalTrack } from "../../utils/track";
4
+ import { useCurrentCallback } from "./useCurrentCallback";
4
5
  export const useTrackManager = ({ deviceManager, tsClient, peerStatus, bandwidthLimits, streamConfig, type, logger, }) => {
5
6
  const currentTrackIdRef = useRef(null);
7
+ const connectionPromiseRef = useRef(null);
6
8
  const { startDevice, stopDevice, enableDevice, disableDevice, deviceTrack, applyMiddleware, currentMiddleware, selectDevice: _selectDevice, } = deviceManager;
7
- const getCurrentTrackId = useCallback(() => {
9
+ // Read live deviceTrack from the `joined` listener without re-subscribing
10
+ // every time it changes.
11
+ const getDeviceTrack = useCurrentCallback(() => deviceTrack);
12
+ const getCurrentTrackId = async () => {
13
+ if (connectionPromiseRef.current) {
14
+ await connectionPromiseRef.current;
15
+ }
8
16
  const refTrackId = currentTrackIdRef.current;
9
17
  if (!refTrackId)
10
18
  return null;
11
19
  const currentTrack = getRemoteOrLocalTrack(tsClient, refTrackId);
12
20
  return currentTrack?.trackId ?? null;
13
- }, [tsClient]);
14
- const selectDevice = useCallback(async (deviceId) => {
21
+ };
22
+ const selectDevice = useCurrentCallback(async (deviceId) => {
15
23
  const result = await _selectDevice(deviceId);
16
24
  if (!result)
17
25
  return;
18
26
  const [newTrack, error] = result;
19
27
  if (error)
20
28
  return error;
21
- const currentTrackId = getCurrentTrackId();
29
+ const currentTrackId = await getCurrentTrackId();
22
30
  if (!currentTrackId)
23
31
  return;
24
32
  await tsClient.replaceTrack(currentTrackId, newTrack);
25
- }, [getCurrentTrackId, _selectDevice, tsClient]);
26
- const setTrackMiddleware = useCallback(async (middleware) => {
33
+ });
34
+ const setTrackMiddleware = useCurrentCallback(async (middleware) => {
27
35
  const processedTrack = await applyMiddleware(middleware);
28
- const currentTrackId = getCurrentTrackId();
36
+ const currentTrackId = await getCurrentTrackId();
29
37
  if (!currentTrackId)
30
38
  return;
31
39
  await tsClient.replaceTrack(currentTrackId, processedTrack);
32
- }, [applyMiddleware, getCurrentTrackId, tsClient]);
33
- const startStreaming = useCallback(async (track, props = { sentQualities: [Variant.VARIANT_LOW, Variant.VARIANT_MEDIUM, Variant.VARIANT_HIGH] }) => {
40
+ });
41
+ const startStreaming = useCurrentCallback(async (track, props = { sentQualities: [Variant.VARIANT_LOW, Variant.VARIANT_MEDIUM, Variant.VARIANT_HIGH] }) => {
34
42
  // temporarily setting the local trackId until we have the remoteTrackId
35
43
  currentTrackIdRef.current = track.id;
36
44
  const trackMetadata = { type, paused: false };
@@ -40,7 +48,9 @@ export const useTrackManager = ({ deviceManager, tsClient, peerStatus, bandwidth
40
48
  }
41
49
  const [maxBandwidth, simulcastConfig] = getConfigAndBandwidthFromProps(props.sentQualities, bandwidthLimits);
42
50
  try {
43
- const remoteTrackId = await tsClient.addTrack(track, trackMetadata, simulcastConfig, maxBandwidth);
51
+ const addTrackJob = tsClient.addTrack(track, trackMetadata, simulcastConfig, maxBandwidth);
52
+ connectionPromiseRef.current = addTrackJob;
53
+ const remoteTrackId = await addTrackJob;
44
54
  currentTrackIdRef.current = remoteTrackId;
45
55
  }
46
56
  catch (err) {
@@ -50,24 +60,24 @@ export const useTrackManager = ({ deviceManager, tsClient, peerStatus, bandwidth
50
60
  }
51
61
  throw err;
52
62
  }
53
- }, [type, tsClient, bandwidthLimits, logger]);
54
- const pauseStreaming = useCallback(async (trackId) => {
63
+ });
64
+ const pauseStreaming = useCurrentCallback(async (trackId) => {
55
65
  if (peerStatus !== "connected")
56
66
  return;
57
67
  await tsClient.replaceTrack(trackId, null);
58
68
  return tsClient.updateTrackMetadata(trackId, { type, paused: true });
59
- }, [tsClient, type, peerStatus]);
60
- const resumeStreaming = useCallback(async (trackId, track) => {
69
+ });
70
+ const resumeStreaming = useCurrentCallback(async (trackId, track) => {
61
71
  if (peerStatus !== "connected")
62
72
  return;
63
73
  await tsClient.replaceTrack(trackId, track);
64
74
  return tsClient.updateTrackMetadata(trackId, { type, paused: false });
65
- }, [peerStatus, tsClient, type]);
75
+ });
66
76
  /**
67
77
  * @see {@link TrackManager#toggleMute} for more details.
68
78
  */
69
- const toggleMute = useCallback(async () => {
70
- const currentTrackId = getCurrentTrackId();
79
+ const toggleMute = useCurrentCallback(async () => {
80
+ const currentTrackId = await getCurrentTrackId();
71
81
  const isTrackCurrentlyEnabled = Boolean(deviceTrack?.enabled);
72
82
  if (!currentTrackId) {
73
83
  logger.warn("Toggling mute is only possible while connected to a room.");
@@ -81,16 +91,16 @@ export const useTrackManager = ({ deviceManager, tsClient, peerStatus, bandwidth
81
91
  enableDevice();
82
92
  await resumeStreaming(currentTrackId, deviceTrack);
83
93
  }
84
- }, [deviceTrack, getCurrentTrackId, logger, disableDevice, pauseStreaming, enableDevice, resumeStreaming]);
94
+ });
85
95
  /**
86
96
  * @see {@link TrackManager#toggleDevice} for more details.
87
97
  */
88
- const toggleDevice = useCallback(async () => {
89
- const currentTrackId = getCurrentTrackId();
98
+ const toggleDevice = useCurrentCallback(async () => {
99
+ const currentTrackId = await getCurrentTrackId();
90
100
  if (deviceTrack) {
91
101
  stopDevice();
92
102
  if (currentTrackId) {
93
- pauseStreaming(currentTrackId);
103
+ await pauseStreaming(currentTrackId);
94
104
  }
95
105
  }
96
106
  else {
@@ -104,22 +114,19 @@ export const useTrackManager = ({ deviceManager, tsClient, peerStatus, bandwidth
104
114
  await startStreaming(newTrack, streamConfig);
105
115
  }
106
116
  }
107
- }, [
108
- getCurrentTrackId,
109
- deviceTrack,
110
- stopDevice,
111
- pauseStreaming,
112
- startDevice,
113
- peerStatus,
114
- resumeStreaming,
115
- startStreaming,
116
- streamConfig,
117
- ]);
117
+ });
118
118
  useEffect(() => {
119
119
  const onJoinedRoom = () => {
120
- if (deviceTrack) {
121
- startStreaming(deviceTrack, streamConfig);
122
- }
120
+ const currentDeviceTrack = getDeviceTrack();
121
+ if (!currentDeviceTrack)
122
+ return;
123
+ // The handler is sync; observe rejections so non-TrackTypeError failures
124
+ // from addTrack don't surface as unhandledrejection.
125
+ void startStreaming(currentDeviceTrack, streamConfig).catch((err) => {
126
+ if (err instanceof TrackTypeError)
127
+ return;
128
+ logger.error(err);
129
+ });
123
130
  };
124
131
  const onLeftRoom = () => {
125
132
  currentTrackIdRef.current = null;
@@ -130,7 +137,7 @@ export const useTrackManager = ({ deviceManager, tsClient, peerStatus, bandwidth
130
137
  tsClient.off("joined", onJoinedRoom);
131
138
  tsClient.off("disconnected", onLeftRoom);
132
139
  };
133
- }, [deviceTrack, startStreaming, tsClient, streamConfig]);
140
+ }, [startStreaming, tsClient, streamConfig, getDeviceTrack, logger]);
134
141
  return {
135
142
  deviceTrack,
136
143
  currentMiddleware,
@@ -1 +1 @@
1
- {"version":3,"file":"useDataChannel.d.ts","sourceRoot":"","sources":["../../src/hooks/useDataChannel.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,oBAAoB,CAsFrD"}
1
+ {"version":3,"file":"useDataChannel.d.ts","sourceRoot":"","sources":["../../src/hooks/useDataChannel.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAG5D;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,oBAAoB,CAyFrD"}
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useContext, useEffect, useState } from "react";
2
2
  import { FishjamClientContext } from "../contexts/fishjamClient";
3
3
  import { PeerStatusContext } from "../contexts/peerStatus";
4
+ import { useCurrentCallback } from "./internal/useCurrentCallback";
4
5
  /**
5
6
  * Hook for data channel operations - publish and subscribe to data.
6
7
  *
@@ -39,7 +40,10 @@ export function useDataChannel() {
39
40
  client.removeListener("disconnected", handleDisconnect);
40
41
  };
41
42
  }, [client]);
42
- const initialize = useCallback(async () => {
43
+ // Stable identity with a live closure: peerStatus / loading / ready must be
44
+ // observed at call time so a captured reference doesn't reject with a stale
45
+ // "Peer is not connected" right after the connect promise settles.
46
+ const initialize = useCurrentCallback(async () => {
43
47
  if (loading || ready)
44
48
  return;
45
49
  if (peerStatus !== "connected") {
@@ -58,7 +62,7 @@ export function useDataChannel() {
58
62
  finally {
59
63
  setLoading(false);
60
64
  }
61
- }, [client, peerStatus, loading, ready]);
65
+ });
62
66
  const publishData = useCallback((data, options) => {
63
67
  try {
64
68
  client.publishData(data, options);
@@ -1 +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"}
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,CA8ClF,CAAC"}
@@ -33,8 +33,14 @@ export const useLocalVAD = (options) => {
33
33
  return;
34
34
  let silenceTicks = 0;
35
35
  let timeoutId;
36
+ const controller = new AbortController();
37
+ const { signal } = controller;
36
38
  const poll = async () => {
39
+ if (signal.aborted)
40
+ return;
37
41
  const trackAudio = await fishjamClient?.current?.getLocalTrackAudioLevel(microphoneTrackId);
42
+ if (signal.aborted)
43
+ return;
38
44
  if (trackAudio != null && trackAudio.level > THRESHOLD) {
39
45
  silenceTicks = 0;
40
46
  setIsSpeaking(true);
@@ -45,10 +51,13 @@ export const useLocalVAD = (options) => {
45
51
  setIsSpeaking(false);
46
52
  }
47
53
  }
54
+ if (signal.aborted)
55
+ return;
48
56
  timeoutId = setTimeout(poll, 100);
49
57
  };
50
58
  timeoutId = setTimeout(poll, 0);
51
59
  return () => {
60
+ controller.abort();
52
61
  clearTimeout(timeoutId);
53
62
  setIsSpeaking(false);
54
63
  };
@@ -1,4 +1,4 @@
1
- import type { Metadata } from "@fishjam-cloud/ts-client";
1
+ import type { Metadata, Variant } from "@fishjam-cloud/ts-client";
2
2
  import type { PeerId, RemoteTrack, Track } from "../types/public";
3
3
  /**
4
4
  *
@@ -38,5 +38,11 @@ export declare function usePeers<PeerMetadata = Record<string, unknown>, ServerM
38
38
  * This property will be removed in future versions.
39
39
  */
40
40
  peers: PeerWithTracks<PeerMetadata, ServerMetadata, RemoteTrack>[];
41
+ /**
42
+ * This function allows to set the quality of tracks received from remote peers.
43
+ * @param trackIds The array of the track ids to set the quality for.
44
+ * @param quality The quality to set for the track.
45
+ */
46
+ setReceivedTracksQuality: (trackIds: string[], quality: Variant) => void;
41
47
  };
42
48
  //# sourceMappingURL=usePeers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePeers.d.ts","sourceRoot":"","sources":["../../src/hooks/usePeers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,QAAQ,EAAiD,MAAM,0BAA0B,CAAC;AAMvH,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAW,MAAM,iBAAiB,CAAC;AAE3E;;;;GAIG;AACH,MAAM,MAAM,cAAc,CAAC,YAAY,EAAE,cAAc,EAAE,CAAC,SAAS,KAAK,GAAG,KAAK,IAAI;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,QAAQ,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IAClD,MAAM,EAAE,CAAC,EAAE,CAAC;IACZ,WAAW,CAAC,EAAE,CAAC,CAAC;IAChB,eAAe,CAAC,EAAE,CAAC,CAAC;IACpB,qBAAqB,CAAC,EAAE,CAAC,CAAC;IAC1B,qBAAqB,CAAC,EAAE,CAAC,CAAC;IAC1B,iBAAiB,EAAE,CAAC,EAAE,CAAC;IACvB,iBAAiB,EAAE,CAAC,EAAE,CAAC;CACxB,CAAC;AAuEF;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAoBrG;;;OAGG;IACH;;OAEG;IACH;;;;OAIG;;EAGN"}
1
+ {"version":3,"file":"usePeers.d.ts","sourceRoot":"","sources":["../../src/hooks/usePeers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,QAAQ,EAAwC,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAMvH,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAW,MAAM,iBAAiB,CAAC;AAE3E;;;;GAIG;AACH,MAAM,MAAM,cAAc,CAAC,YAAY,EAAE,cAAc,EAAE,CAAC,SAAS,KAAK,GAAG,KAAK,IAAI;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,QAAQ,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IAClD,MAAM,EAAE,CAAC,EAAE,CAAC;IACZ,WAAW,CAAC,EAAE,CAAC,CAAC;IAChB,eAAe,CAAC,EAAE,CAAC,CAAC;IACpB,qBAAqB,CAAC,EAAE,CAAC,CAAC;IAC1B,qBAAqB,CAAC,EAAE,CAAC,CAAC;IAC1B,iBAAiB,EAAE,CAAC,EAAE,CAAC;IACvB,iBAAiB,EAAE,CAAC,EAAE,CAAC;CACxB,CAAC;AAuEF;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IA0BrG;;;OAGG;IACH;;OAEG;IACH;;;;OAIG;;IAEH;;;;OAIG;yCAvBQ,MAAM,EAAE,WAAW,OAAO;EA0BxC"}
@@ -1,4 +1,4 @@
1
- import { useContext } from "react";
1
+ import { useCallback, useContext } from "react";
2
2
  import { FishjamClientContext } from "../contexts/fishjamClient";
3
3
  import { FishjamClientStateContext } from "../contexts/fishjamState";
4
4
  function trackContextToTrack(track) {
@@ -63,6 +63,7 @@ export function usePeers() {
63
63
  ? getLocalPeerWithTracks(clientState.localPeer)
64
64
  : null;
65
65
  const remotePeers = Object.values(clientState.peers).map((peer) => getRemotePeerWithTracks(peer, fishjamClient.current));
66
+ const setReceivedTracksQuality = useCallback((trackIds, quality) => trackIds.forEach((trackId) => fishjamClient.current.setTargetTrackEncoding(trackId, quality)), [fishjamClient]);
66
67
  return {
67
68
  /**
68
69
  * The local peer with distinguished tracks (camera, microphone, screen share).
@@ -77,5 +78,11 @@ export function usePeers() {
77
78
  * This property will be removed in future versions.
78
79
  */
79
80
  peers: remotePeers,
81
+ /**
82
+ * This function allows to set the quality of tracks received from remote peers.
83
+ * @param trackIds The array of the track ids to set the quality for.
84
+ * @param quality The quality to set for the track.
85
+ */
86
+ setReceivedTracksQuality,
80
87
  };
81
88
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useScreenShare.d.ts","sourceRoot":"","sources":["../../src/hooks/useScreenShare.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,cAAc;IAKvB;;OAEG;;wBADiE,CAAC;wBAEpD,CAAC;;IAClB;;OAEG;;IAEH;;OAEG;;IAEH;;OAEG;;IAEH;;OAEG;;IAEH;;;;OAIG;;IAEH;;;;OAIG;;CAGN,CAAC"}
1
+ {"version":3,"file":"useScreenShare.d.ts","sourceRoot":"","sources":["../../src/hooks/useScreenShare.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,cAAc;IAKvB;;OAEG;;wBACiB,CAAC;wBAER,CAAC;;IADd;;OAEG;;IAEH;;OAEG;;IAEH;;OAEG;;IAEH;;OAEG;;IAEH;;;;OAIG;;IAEH;;;;OAIG;;CAGN,CAAC"}
@@ -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;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
+ {"version":3,"file":"useVAD.d.ts","sourceRoot":"","sources":["../../src/hooks/useVAD.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAG9C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,MAAM,GAAI,SAAS;IAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAAE,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAyD1F,CAAC"}
@@ -1,4 +1,4 @@
1
- import { useContext, useEffect, useMemo, useState } from "react";
1
+ import { useContext, useEffect, useMemo, useReducer } from "react";
2
2
  import { FishjamClientStateContext } from "../contexts/fishjamState";
3
3
  import { useLocalVAD } from "./useLocalVAD";
4
4
  /**
@@ -40,37 +40,33 @@ export const useVAD = (options) => {
40
40
  peerId: peer.id,
41
41
  microphoneTrack: Array.from(peer.tracks.values()).find(({ metadata }) => metadata?.type === "microphone"),
42
42
  })), [clientState.peers, peerIds]);
43
- const getDefaultVadStatuses = () => micTracksWithSelectedPeerIds.reduce((mappedTracks, { peerId, microphoneTrack }) => ({
44
- ...mappedTracks,
45
- [peerId]: microphoneTrack ? { [microphoneTrack.trackId]: microphoneTrack.vadStatus } : {},
46
- }), {});
47
- const [_vadStatuses, setVadStatuses] = useState(getDefaultVadStatuses);
43
+ // `voiceActivityChanged` mutates the track context in place and does not flow through
44
+ // `useFishjamClientState`, so we need an explicit re-render trigger to re-read the
45
+ // current `vadStatus` off each mic track.
46
+ const [version, bumpVersion] = useReducer((n) => n + 1, 0);
48
47
  useEffect(() => {
49
- const unsubs = micTracksWithSelectedPeerIds.map(({ peerId, microphoneTrack }) => {
50
- const updateVadStatus = (track) => {
51
- setVadStatuses((prev) => ({
52
- ...prev,
53
- [peerId]: { ...prev[peerId], [track.trackId]: track.vadStatus },
54
- }));
55
- };
56
- if (microphoneTrack) {
57
- microphoneTrack.on("voiceActivityChanged", updateVadStatus);
58
- }
48
+ const unsubs = micTracksWithSelectedPeerIds.map(({ microphoneTrack }) => {
49
+ if (!microphoneTrack)
50
+ return () => { };
51
+ microphoneTrack.on("voiceActivityChanged", bumpVersion);
59
52
  return () => {
60
- if (microphoneTrack) {
61
- microphoneTrack.off("voiceActivityChanged", updateVadStatus);
62
- }
53
+ microphoneTrack.off("voiceActivityChanged", bumpVersion);
63
54
  };
64
55
  });
65
56
  return () => unsubs.forEach((unsub) => unsub());
66
57
  }, [micTracksWithSelectedPeerIds]);
67
58
  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]);
59
+ const vadStatuses = useMemo(() => {
60
+ // Referencing `version` makes the memo recompute on every `voiceActivityChanged`
61
+ // event, so we re-read each current mic track's mutable `vadStatus`.
62
+ void version;
63
+ return {
64
+ ...Object.fromEntries(micTracksWithSelectedPeerIds.map(({ peerId, microphoneTrack }) => [
65
+ peerId,
66
+ microphoneTrack?.vadStatus === "speech",
67
+ ])),
68
+ ...localVAD,
69
+ };
70
+ }, [micTracksWithSelectedPeerIds, localVAD, version]);
75
71
  return vadStatuses;
76
72
  };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useCurrentCallback.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCurrentCallback.spec.d.ts","sourceRoot":"","sources":["../../src/tests/useCurrentCallback.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,49 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { useCurrentCallback } from "../hooks/internal/useCurrentCallback";
4
+ describe("useCurrentCallback", () => {
5
+ it("returns a stable function reference across re-renders", () => {
6
+ const { result, rerender } = renderHook((handler) => useCurrentCallback(handler), {
7
+ initialProps: () => undefined,
8
+ });
9
+ const firstRef = result.current;
10
+ rerender(() => undefined);
11
+ rerender(() => undefined);
12
+ expect(result.current).toBe(firstRef);
13
+ });
14
+ it("invokes the latest handler when called after a re-render", () => {
15
+ const first = vi.fn();
16
+ const second = vi.fn();
17
+ const { result, rerender } = renderHook((handler) => useCurrentCallback(handler), {
18
+ initialProps: first,
19
+ });
20
+ result.current();
21
+ expect(first).toHaveBeenCalledTimes(1);
22
+ expect(second).not.toHaveBeenCalled();
23
+ rerender(second);
24
+ result.current();
25
+ expect(first).toHaveBeenCalledTimes(1);
26
+ expect(second).toHaveBeenCalledTimes(1);
27
+ });
28
+ it("forwards arguments and returns the handler's value", () => {
29
+ const handler = vi.fn((a, b) => a + b);
30
+ const { result } = renderHook(() => useCurrentCallback(handler));
31
+ const sum = result.current(2, 3);
32
+ expect(handler).toHaveBeenCalledWith(2, 3);
33
+ expect(sum).toBe(5);
34
+ });
35
+ it("the wrapper captured before a re-render still calls the latest handler", () => {
36
+ const first = vi.fn(() => "first");
37
+ const second = vi.fn(() => "second");
38
+ const { result, rerender } = renderHook((handler) => useCurrentCallback(handler), {
39
+ initialProps: first,
40
+ });
41
+ // Simulates a consumer capturing the reference (e.g. inside a useEffect
42
+ // closure) before the handler closure has changed.
43
+ const captured = result.current;
44
+ rerender(second);
45
+ expect(captured()).toBe("second");
46
+ expect(second).toHaveBeenCalledTimes(1);
47
+ expect(first).not.toHaveBeenCalled();
48
+ });
49
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishjam-cloud/react-client",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
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": "0.26.0",
65
+ "@fishjam-cloud/ts-client": "0.26.1",
66
66
  "events": "3.3.0",
67
67
  "lodash.isequal": "4.5.0"
68
68
  },