|
1 | | -import type { TrackContext, VadStatus } from "@fishjam-cloud/ts-client"; |
| 1 | +import type { FishjamTrackContext, VadStatus } from "@fishjam-cloud/ts-client"; |
2 | 2 | import { useContext, useEffect, useMemo, useState } from "react"; |
3 | 3 |
|
4 | 4 | import { FishjamClientStateContext } from "../contexts/fishjamState"; |
5 | 5 | import type { PeerId, TrackId } from "../types/public"; |
| 6 | +import { useLocalVAD } from "./useLocalVAD"; |
6 | 7 |
|
7 | 8 | /** |
8 | | - * Voice activity detection. Use this hook to check if voice is detected in audio track for given peer(s). |
| 9 | + * Voice activity detection. Use this hook to check if voice is detected in the audio track for given peer(s). |
9 | 10 | * |
10 | | - * @param options - Options object containing `peerIds` - a list of ids of peers to subscribe to for voice activity detection notifications. |
| 11 | + * Remote peer VAD is driven by `vadNotification` messages from the backend. |
| 12 | + * If the local peer's id is included in `peerIds`, local VAD is determined client-side |
| 13 | + * by polling the microphone's audio level (see `useLocalVAD`). |
| 14 | + * |
| 15 | + * @param options - Options object. |
| 16 | + * @param options.peerIds - List of peer ids to subscribe to for VAD notifications. |
| 17 | + * Include the local peer's id to also track whether the local user is speaking. |
11 | 18 | * |
12 | 19 | * Example usage: |
13 | 20 | * ```tsx |
14 | 21 | * import { useVAD, type PeerId } from "@fishjam-cloud/react-client"; |
| 22 | + * |
15 | 23 | * function WhoIsTalkingComponent({ peerIds }: { peerIds: PeerId[] }) { |
16 | | - * const peersInfo = useVAD({peerIds}); |
| 24 | + * const peersInfo = useVAD({ peerIds }); |
17 | 25 | * const activePeers = (Object.keys(peersInfo) as PeerId[]).filter((peerId) => peersInfo[peerId]); |
18 | 26 | * |
19 | 27 | * return "Now talking: " + activePeers.join(", "); |
20 | 28 | * } |
21 | 29 | * ``` |
22 | 30 | * @category Connection |
23 | 31 | * @group Hooks |
24 | | - * @returns Each key is a peerId and the boolean value indicates if voice activity is currently detected for that peer. |
| 32 | + * @returns A record where each key is a peer id and the boolean value indicates |
| 33 | + * whether voice activity is currently detected for that peer. |
25 | 34 | */ |
26 | 35 | export const useVAD = (options: { peerIds: ReadonlyArray<PeerId> }): Record<PeerId, boolean> => { |
27 | 36 | const { peerIds } = options; |
28 | 37 | const clientState = useContext(FishjamClientStateContext); |
29 | 38 | if (!clientState) throw Error("useVAD must be used within FishjamProvider"); |
| 39 | + const showLocalPeerVAD = useMemo( |
| 40 | + () => (clientState.localPeer?.id ? peerIds.includes(clientState.localPeer?.id) : false), |
| 41 | + [clientState.localPeer?.id, peerIds], |
| 42 | + ); |
30 | 43 |
|
31 | 44 | const micTracksWithSelectedPeerIds = useMemo( |
32 | 45 | () => |
33 | 46 | Object.values(clientState.peers) |
34 | 47 | .filter((peer) => peerIds.includes(peer.id)) |
35 | 48 | .map((peer) => ({ |
36 | 49 | peerId: peer.id, |
37 | | - microphoneTracks: Array.from(peer.tracks.values()).filter(({ metadata }) => metadata?.type === "microphone"), |
| 50 | + microphoneTrack: Array.from(peer.tracks.values()).find(({ metadata }) => metadata?.type === "microphone"), |
38 | 51 | })), |
39 | 52 | [clientState.peers, peerIds], |
40 | 53 | ); |
41 | 54 |
|
42 | 55 | const getDefaultVadStatuses = () => |
43 | 56 | micTracksWithSelectedPeerIds.reduce<Record<PeerId, Record<TrackId, VadStatus>>>( |
44 | | - (mappedTracks, peer) => ({ |
| 57 | + (mappedTracks, { peerId, microphoneTrack }) => ({ |
45 | 58 | ...mappedTracks, |
46 | | - [peer.peerId]: peer.microphoneTracks.reduce( |
47 | | - (vadStatuses, track) => ({ ...vadStatuses, [track.trackId]: track.vadStatus }), |
48 | | - {}, |
49 | | - ), |
| 59 | + [peerId]: microphoneTrack ? { [microphoneTrack.trackId]: microphoneTrack.vadStatus } : {}, |
50 | 60 | }), |
51 | 61 | {}, |
52 | 62 | ); |
53 | 63 |
|
54 | 64 | const [_vadStatuses, setVadStatuses] = useState<Record<PeerId, Record<TrackId, VadStatus>>>(getDefaultVadStatuses); |
55 | 65 |
|
56 | 66 | useEffect(() => { |
57 | | - const unsubs = micTracksWithSelectedPeerIds.map(({ peerId, microphoneTracks }) => { |
58 | | - const updateVadStatus = (track: TrackContext) => { |
| 67 | + const unsubs = micTracksWithSelectedPeerIds.map(({ peerId, microphoneTrack }) => { |
| 68 | + const updateVadStatus = (track: FishjamTrackContext) => { |
59 | 69 | setVadStatuses((prev) => ({ |
60 | 70 | ...prev, |
61 | 71 | [peerId]: { ...prev[peerId], [track.trackId]: track.vadStatus }, |
62 | 72 | })); |
63 | 73 | }; |
64 | 74 |
|
65 | | - microphoneTracks.forEach((track) => { |
66 | | - track.on("voiceActivityChanged", updateVadStatus); |
67 | | - }); |
| 75 | + if (microphoneTrack) { |
| 76 | + microphoneTrack.on("voiceActivityChanged", updateVadStatus); |
| 77 | + } |
68 | 78 |
|
69 | 79 | return () => { |
70 | | - microphoneTracks.forEach((track) => { |
71 | | - track.off("voiceActivityChanged", updateVadStatus); |
72 | | - }); |
| 80 | + if (microphoneTrack) { |
| 81 | + microphoneTrack.off("voiceActivityChanged", updateVadStatus); |
| 82 | + } |
73 | 83 | }; |
74 | 84 | }); |
75 | 85 |
|
76 | 86 | return () => unsubs.forEach((unsub) => unsub()); |
77 | 87 | }, [micTracksWithSelectedPeerIds]); |
78 | 88 |
|
| 89 | + const localVAD = useLocalVAD({ disabled: !showLocalPeerVAD }); |
| 90 | + |
79 | 91 | const vadStatuses = useMemo( |
80 | 92 | () => |
81 | | - Object.fromEntries( |
82 | | - Object.entries(_vadStatuses).map(([peerId, tracks]) => [ |
83 | | - peerId, |
84 | | - Object.values(tracks).some((vad) => vad === "speech"), |
85 | | - ]), |
86 | | - ) satisfies Record<PeerId, boolean>, |
87 | | - [_vadStatuses], |
| 93 | + ({ |
| 94 | + ...Object.fromEntries( |
| 95 | + Object.entries(_vadStatuses).map(([peerId, tracks]) => [ |
| 96 | + peerId, |
| 97 | + Object.values(tracks).some((vad) => vad === "speech"), |
| 98 | + ]), |
| 99 | + ), |
| 100 | + ...localVAD, |
| 101 | + }) satisfies Record<PeerId, boolean>, |
| 102 | + [_vadStatuses, localVAD], |
88 | 103 | ); |
89 | 104 |
|
90 | 105 | return vadStatuses; |
|
0 commit comments