diff --git a/examples/react-client/fishjam-chat/package.json b/examples/react-client/fishjam-chat/package.json index 63b7e944..a5611cef 100644 --- a/examples/react-client/fishjam-chat/package.json +++ b/examples/react-client/fishjam-chat/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --fix", "lint:check": "eslint . --ext .ts,.tsx", "format": "prettier --write . --ignore-path ./.eslintignore", "format:check": "prettier --check . --ignore-path ./.eslintignore", diff --git a/examples/react-client/fishjam-chat/src/components/RoomView.tsx b/examples/react-client/fishjam-chat/src/components/RoomView.tsx index 23c5e0f8..31671569 100644 --- a/examples/react-client/fishjam-chat/src/components/RoomView.tsx +++ b/examples/react-client/fishjam-chat/src/components/RoomView.tsx @@ -31,7 +31,11 @@ export const RoomView = () => { > {localPeer && ( <> - + {localPeer.screenShareVideoTrack && ( diff --git a/packages/react-client/src/FishjamProvider.tsx b/packages/react-client/src/FishjamProvider.tsx index 559107a6..15e04895 100644 --- a/packages/react-client/src/FishjamProvider.tsx +++ b/packages/react-client/src/FishjamProvider.tsx @@ -7,42 +7,61 @@ import { useFishjamClientState } from "./hooks/internal/useFishjamClientState"; import type { FishjamContextType } from "./hooks/internal/useFishjamContext"; import { FishjamContext } from "./hooks/internal/useFishjamContext"; import { usePeerStatus } from "./hooks/internal/usePeerStatus"; +import { useScreenShareManager } from "./hooks/internal/useScreenshareManager"; import { useTrackManager } from "./hooks/internal/useTrackManager"; -import { useScreenShareManager } from "./hooks/useScreenShare"; -import type { BandwidthLimits, PersistLastDeviceHandlers, StartStreamingProps } from "./types/public"; +import type { BandwidthLimits, PersistLastDeviceHandlers, StreamConfig } from "./types/public"; import { mergeWithDefaultBandwitdthLimits } from "./utils/bandwidth"; -interface FishjamProviderProps extends PropsWithChildren { +/** + * @category Components + */ +export interface FishjamProviderProps extends PropsWithChildren { + /** + * Use {@link ReconnectConfig} to adjust reconnection policy to your needs or set false it. + * Set to true by default. + */ reconnect?: ReconnectConfig | boolean; + /** + * Set preferred constraints. + * @param {MediaStreamConstraints} constraints - The media stream constraints as defined by the Web API. + * @external {@link https://udn.realityripple.com/docs/Web/API/MediaStreamConstraints MediaStreamConstraints} + */ constraints?: Pick; + /** + * Decide if you want Fishjam SDK to persist last used device in the local storage. + * You can also provide your getter and setter by using the {@link PersistLastDeviceHandlers} interface. + */ persistLastDevice?: boolean | PersistLastDeviceHandlers; + /** + * Adjust max bandwidth limit for a single stream and simulcast. + */ bandwidthLimits?: Partial; - autoStreamCamera?: StartStreamingProps; - autoStreamMicrophone?: StartStreamingProps; + /** + * Configure whether to use video simulcast and which layers to send if so. + */ + videoConfig?: StreamConfig; + /** + * Configure whether to use audio simulcast and which layers to send if so. + */ + audioConfig?: StreamConfig; } /** + * Provides the Fishjam Context * @category Components + * @param */ -export function FishjamProvider({ - children, - reconnect, - constraints, - persistLastDevice, - bandwidthLimits, - autoStreamCamera, - autoStreamMicrophone, -}: FishjamProviderProps) { - const fishjamClientRef = useRef(new FishjamClient({ reconnect })); +export function FishjamProvider(props: FishjamProviderProps) { + const fishjamClientRef = useRef(new FishjamClient({ reconnect: props.reconnect })); const hasDevicesBeenInitializedRef = useRef(false); - const storage = persistLastDevice; + const storage = props.persistLastDevice; const videoDeviceManagerRef = useRef( new DeviceManager({ deviceType: "video", defaultConstraints: VIDEO_TRACK_CONSTRAINTS, - userConstraints: constraints?.video, + userConstraints: props.constraints?.video, storage, }), ); @@ -51,21 +70,21 @@ export function FishjamProvider({ new DeviceManager({ deviceType: "audio", defaultConstraints: AUDIO_TRACK_CONSTRAINTS, - userConstraints: constraints?.audio, + userConstraints: props.constraints?.audio, storage, }), ); const { peerStatus, getCurrentPeerStatus } = usePeerStatus(fishjamClientRef.current); - const mergedBandwidthLimits = mergeWithDefaultBandwitdthLimits(bandwidthLimits); + const mergedBandwidthLimits = mergeWithDefaultBandwitdthLimits(props.bandwidthLimits); const videoTrackManager = useTrackManager({ mediaManager: videoDeviceManagerRef.current, tsClient: fishjamClientRef.current, getCurrentPeerStatus, bandwidthLimits: mergedBandwidthLimits, - autoStreamProps: autoStreamCamera, + streamConfig: props.videoConfig, }); const audioTrackManager = useTrackManager({ @@ -73,7 +92,7 @@ export function FishjamProvider({ tsClient: fishjamClientRef.current, getCurrentPeerStatus, bandwidthLimits: mergedBandwidthLimits, - autoStreamProps: autoStreamMicrophone, + streamConfig: props.audioConfig, }); const screenShareManager = useScreenShareManager({ fishjamClient: fishjamClientRef.current, getCurrentPeerStatus }); @@ -93,5 +112,5 @@ export function FishjamProvider({ bandwidthLimits: mergedBandwidthLimits, }; - return {children}; + return {props.children}; } diff --git a/packages/react-client/src/hooks/devices/useCamera.ts b/packages/react-client/src/hooks/devices/useCamera.ts index 13522b74..ad469054 100644 --- a/packages/react-client/src/hooks/devices/useCamera.ts +++ b/packages/react-client/src/hooks/devices/useCamera.ts @@ -3,16 +3,45 @@ import type { DeviceItem, TrackMiddleware } from "../../types/public"; import { useDeviceApi } from "../internal/device/useDeviceApi"; import { useFishjamContext } from "../internal/useFishjamContext"; -type CameraApi = { +/** + * @category Devices + */ +export type UseCameraResult = { + /** + * Toggles current camera on/off + */ toggleCamera: () => void; - // TODO: use branded type once it's added + /** + * Selects the camera device + */ selectCamera: (deviceId: string) => void; + /** + * Indicates which camera is now turned on and streaming + */ activeCamera: DeviceItem | null; + /** + * Indicates whether the microphone is streaming video + */ isCameraOn: boolean; + /** + * The MediaStream object containing the current stream + */ cameraStream: MediaStream | null; + /** + * The currently set camera middleware function + */ currentCameraMiddleware: TrackMiddleware; + /** + * Sets the camera middleware + */ setCameraTrackMiddleware: (middleware: TrackMiddleware | null) => Promise; + /** + * List of available camera devices + */ cameraDevices: DeviceItem[]; + /** + * Possible error thrown while setting up the camera + */ cameraDeviceError: DeviceError | null; }; @@ -20,42 +49,19 @@ type CameraApi = { * * @category Devices */ -export function useCamera(): CameraApi { +export function useCamera(): UseCameraResult { const { videoTrackManager, videoDeviceManagerRef } = useFishjamContext(); const deviceApi = useDeviceApi({ deviceManager: videoDeviceManagerRef.current }); return { - /** Toggles current camera on/off */ toggleCamera: videoTrackManager.toggleDevice, - /** Selects the camera device */ selectCamera: videoTrackManager.selectDevice, - /** - * Indicates which camera is now turned on and streaming - */ activeCamera: deviceApi.activeDevice, - /** - * Indicates whether the microphone is streaming video - */ isCameraOn: !!deviceApi.mediaStream, - /** - * The MediaStream object containing the current stream - */ cameraStream: deviceApi.mediaStream, - /** - * The currently set camera middleware function - */ currentCameraMiddleware: deviceApi.currentMiddleware, - /** - * Sets the camera middleware - */ setCameraTrackMiddleware: videoTrackManager.setTrackMiddleware, - /** - * List of available camera devices - */ cameraDevices: deviceApi.devices, - /** - * Possible error thrown while setting up the camera - */ cameraDeviceError: deviceApi.deviceError, }; } diff --git a/packages/react-client/src/hooks/devices/useInitializeDevices.ts b/packages/react-client/src/hooks/devices/useInitializeDevices.ts index 7831a655..44dfe971 100644 --- a/packages/react-client/src/hooks/devices/useInitializeDevices.ts +++ b/packages/react-client/src/hooks/devices/useInitializeDevices.ts @@ -5,10 +5,16 @@ import { getAvailableMedia, getCorrectedResult } from "../../devices/mediaInitia import { useFishjamContext } from "../internal/useFishjamContext"; /** - * * @category Devices */ -export const useInitializeDevices = () => { +export type UseInitializeDevicesResult = { + initializeDevices: () => Promise; +}; + +/** + * @category Devices + */ +export const useInitializeDevices = (): UseInitializeDevicesResult => { const { videoDeviceManagerRef, audioDeviceManagerRef, hasDevicesBeenInitializedRef } = useFishjamContext(); const initializeDevices = useCallback(async () => { diff --git a/packages/react-client/src/hooks/devices/useMicrophone.ts b/packages/react-client/src/hooks/devices/useMicrophone.ts index 89510853..ee34f91b 100644 --- a/packages/react-client/src/hooks/devices/useMicrophone.ts +++ b/packages/react-client/src/hooks/devices/useMicrophone.ts @@ -3,18 +3,47 @@ import type { DeviceItem, TrackMiddleware } from "../../types/public"; import { useDeviceApi } from "../internal/device/useDeviceApi"; import { useFishjamContext } from "../internal/useFishjamContext"; -type MicrophoneApi = { +/** + * @category Devices + */ +export type UseMicrophoneResult = { + /** Toggles current microphone on/off */ toggleMicrophone: () => void; + /** Mutes/unmutes the microphone */ toggleMicrophoneMute: () => void; - // TODO: use branded type once it's added + /** Selects the microphone device */ selectMicrophone: (deviceId: string) => void; + /** + * Indicates which microphone is now turned on and streaming audio + */ activeMicrophone: DeviceItem | null; + /** + * Indicates whether the microphone is streaming audio + */ isMicrophoneOn: boolean; + /** + * Indicates whether the microphone is muted + */ isMicrophoneMuted: boolean; + /** + * The MediaStream object containing the current audio stream + */ microphoneStream: MediaStream | null; + /** + * The currently set microphone middleware function + */ currentMicrophoneMiddleware: TrackMiddleware; + /** + * Sets the microphone middleware + */ setMicrophoneTrackMiddleware: (middleware: TrackMiddleware | null) => Promise; + /** + * List of available microphone devices + */ microphoneDevices: DeviceItem[]; + /** + * Possible error thrown while setting up the microphone + */ microphoneDeviceError: DeviceError | null; }; @@ -22,48 +51,21 @@ type MicrophoneApi = { * * @category Devices */ -export function useMicrophone(): MicrophoneApi { +export function useMicrophone(): UseMicrophoneResult { const { audioTrackManager, audioDeviceManagerRef } = useFishjamContext(); const deviceApi = useDeviceApi({ deviceManager: audioDeviceManagerRef.current }); return { - /** Toggles current microphone on/off */ toggleMicrophone: audioTrackManager.toggleDevice, - /** Mutes/unmutes the microphone */ toggleMicrophoneMute: audioTrackManager.toggleMute, - /** Selects the microphone device */ selectMicrophone: audioTrackManager.selectDevice, - /** - * Indicates which microphone is now turned on and streaming audio - */ activeMicrophone: deviceApi.activeDevice, - /** - * Indicates whether the microphone is streaming audio - */ isMicrophoneOn: !!deviceApi.mediaStream, - /** - * Indicates whether the microphone is muted - */ isMicrophoneMuted: audioTrackManager.paused, - /** - * The MediaStream object containing the current audio stream - */ microphoneStream: deviceApi.mediaStream, - /** - * The currently set microphone middleware function - */ currentMicrophoneMiddleware: deviceApi.currentMiddleware, - /** - * Sets the microphone middleware - */ setMicrophoneTrackMiddleware: audioTrackManager.setTrackMiddleware, - /** - * List of available microphone devices - */ microphoneDevices: deviceApi.devices, - /** - * Possible error thrown while setting up the microphone - */ microphoneDeviceError: deviceApi.deviceError, }; } diff --git a/packages/react-client/src/hooks/internal/useFishjamContext.ts b/packages/react-client/src/hooks/internal/useFishjamContext.ts index 5b3396d7..9ed5360d 100644 --- a/packages/react-client/src/hooks/internal/useFishjamContext.ts +++ b/packages/react-client/src/hooks/internal/useFishjamContext.ts @@ -3,7 +3,8 @@ import { createContext, type MutableRefObject, useContext } from "react"; import type { DeviceManager } from "../../devices/DeviceManager"; import type { TrackManager } from "../../types/internal"; -import type { BandwidthLimits, PeerStatus, ScreenshareApi } from "../../types/public"; +import type { BandwidthLimits, PeerStatus } from "../../types/public"; +import type { UseScreenshareResult } from "../useScreenShare"; import type { FishjamClientState } from "./useFishjamClientState"; export type FishjamContextType = { @@ -11,7 +12,7 @@ export type FishjamContextType = { videoDeviceManagerRef: MutableRefObject; audioDeviceManagerRef: MutableRefObject; hasDevicesBeenInitializedRef: MutableRefObject; - screenShareManager: ScreenshareApi; + screenShareManager: UseScreenshareResult; peerStatus: PeerStatus; videoTrackManager: TrackManager; audioTrackManager: TrackManager; diff --git a/packages/react-client/src/hooks/internal/useScreenshareManager.ts b/packages/react-client/src/hooks/internal/useScreenshareManager.ts new file mode 100644 index 00000000..239b3dc6 --- /dev/null +++ b/packages/react-client/src/hooks/internal/useScreenshareManager.ts @@ -0,0 +1,160 @@ +import type { FishjamClient } from "@fishjam-cloud/ts-client"; +import { useCallback, useEffect, useRef, useState } from "react"; + +import type { ScreenShareState } from "../../types/internal"; +import type { PeerStatus, TracksMiddleware } from "../../types/public"; +import { getRemoteOrLocalTrack } from "../../utils/track"; +import type { UseScreenshareResult } from "../useScreenShare"; + +interface ScreenShareManagerProps { + fishjamClient: FishjamClient; + getCurrentPeerStatus: () => PeerStatus; +} + +export const useScreenShareManager = ({ + fishjamClient, + getCurrentPeerStatus, +}: ScreenShareManagerProps): UseScreenshareResult => { + const [state, setState] = useState({ stream: null, trackIds: null }); + + const cleanMiddlewareFnRef = useRef<(() => void) | null>(null); + + const stream = state.stream ?? null; + const [mediaVideoTrack, mediaAudioTrack] = stream ? getTracksFromStream(stream) : [null, null]; + + const getDisplayName = () => { + const name = fishjamClient.getLocalPeer()?.metadata?.peer?.displayName; + if (typeof name === "string") return name; + }; + + const startStreaming: UseScreenshareResult["startStreaming"] = async (props) => { + const displayStream = await navigator.mediaDevices.getDisplayMedia({ + video: props?.videoConstraints ?? true, + audio: props?.audioConstraints ?? true, + }); + + const displayName = getDisplayName(); + + let [video, audio] = getTracksFromStream(displayStream); + + if (state.tracksMiddleware) { + const { videoTrack, audioTrack, onClear } = state.tracksMiddleware(video, audio); + video = videoTrack; + audio = audioTrack; + cleanMiddlewareFnRef.current = onClear; + } + + const addTrackPromises = [fishjamClient.addTrack(video, { displayName, type: "screenShareVideo", paused: false })]; + if (audio) + addTrackPromises.push(fishjamClient.addTrack(audio, { displayName, type: "screenShareAudio", paused: false })); + + const [videoId, audioId] = await Promise.all(addTrackPromises); + setState({ stream: displayStream, trackIds: { videoId, audioId } }); + }; + + const replaceTracks = async (newVideoTrack: MediaStreamTrack, newAudioTrack: MediaStreamTrack | null) => { + if (!state?.stream) return; + + const addTrackPromises = [fishjamClient.replaceTrack(state.trackIds.videoId, newVideoTrack)]; + + if (newAudioTrack && state.trackIds.audioId) { + addTrackPromises.push(fishjamClient.replaceTrack(state.trackIds.audioId, newAudioTrack)); + } + + await Promise.all(addTrackPromises); + }; + + const cleanMiddleware = useCallback(() => { + cleanMiddlewareFnRef.current?.(); + cleanMiddlewareFnRef.current = null; + }, []); + + const setTracksMiddleware = async (middleware: TracksMiddleware | null): Promise => { + if (!state?.stream) return; + + const [video, audio] = getTracksFromStream(state.stream); + + cleanMiddleware(); + + const { videoTrack, audioTrack, onClear } = middleware?.(video, audio) ?? { + videoTrack: video, + audioTrack: audio, + onClear: null, + }; + cleanMiddlewareFnRef.current = onClear; + await replaceTracks(videoTrack, audioTrack); + }; + + const stopStreaming: UseScreenshareResult["stopStreaming"] = useCallback(async () => { + if (!state.stream) { + console.warn("No stream to stop"); + return; + } + const [video, audio] = getTracksFromStream(state.stream); + + video.stop(); + if (audio) audio.stop(); + + if (getCurrentPeerStatus() === "connected") { + const removeTrackPromises = [fishjamClient.removeTrack(state.trackIds.videoId)]; + if (state.trackIds.audioId) removeTrackPromises.push(fishjamClient.removeTrack(state.trackIds.audioId)); + + await Promise.all(removeTrackPromises); + } + + cleanMiddleware(); + setState(({ tracksMiddleware }) => ({ stream: null, trackIds: null, tracksMiddleware })); + }, [state, fishjamClient, setState, cleanMiddleware, getCurrentPeerStatus]); + + useEffect(() => { + if (!state.stream) return; + const [video, audio] = getTracksFromStream(state.stream); + + const trackEndedHandler = () => { + stopStreaming(); + }; + + video.addEventListener("ended", trackEndedHandler); + audio?.addEventListener("ended", trackEndedHandler); + + return () => { + video.removeEventListener("ended", trackEndedHandler); + audio?.removeEventListener("ended", trackEndedHandler); + }; + }, [state, stopStreaming]); + + useEffect(() => { + const onDisconnected = () => { + if (stream) { + stopStreaming(); + } + }; + fishjamClient.on("disconnected", onDisconnected); + + return () => { + fishjamClient.removeListener("disconnected", onDisconnected); + }; + }, [stopStreaming, fishjamClient, stream]); + + const videoBroadcast = state.stream ? getRemoteOrLocalTrack(fishjamClient, state.trackIds.videoId) : null; + const audioBroadcast = state.trackIds?.audioId ? getRemoteOrLocalTrack(fishjamClient, state.trackIds.audioId) : null; + + return { + startStreaming, + stopStreaming, + stream, + videoTrack: mediaVideoTrack, + audioTrack: mediaAudioTrack, + videoBroadcast, + audioBroadcast, + setTracksMiddleware, + currentTracksMiddleware: state?.tracksMiddleware ?? null, + }; +}; + +const getTracksFromStream = (stream: MediaStream): [MediaStreamTrack, MediaStreamTrack | null] => { + const video = stream.getVideoTracks()[0]; + const audio = stream.getAudioTracks()[0] ?? null; + + return [video, audio]; +}; diff --git a/packages/react-client/src/hooks/internal/useTrackManager.ts b/packages/react-client/src/hooks/internal/useTrackManager.ts index 64345515..40df72d0 100644 --- a/packages/react-client/src/hooks/internal/useTrackManager.ts +++ b/packages/react-client/src/hooks/internal/useTrackManager.ts @@ -2,7 +2,7 @@ import { type FishjamClient, type TrackMetadata, Variant } from "@fishjam-cloud/ import { useCallback, useEffect, useMemo, useState } from "react"; import type { MediaManager, TrackManager } from "../../types/internal"; -import type { BandwidthLimits, PeerStatus, StartStreamingProps, TrackMiddleware } from "../../types/public"; +import type { BandwidthLimits, PeerStatus, StreamConfig, TrackMiddleware } from "../../types/public"; import { getConfigAndBandwidthFromProps, getRemoteOrLocalTrack } from "../../utils/track"; interface TrackManagerConfig { @@ -10,7 +10,7 @@ interface TrackManagerConfig { tsClient: FishjamClient; getCurrentPeerStatus: () => PeerStatus; bandwidthLimits: BandwidthLimits; - autoStreamProps?: StartStreamingProps; + streamConfig?: StreamConfig; } type ToggleMode = "hard" | "soft"; @@ -27,7 +27,7 @@ export const useTrackManager = ({ tsClient, getCurrentPeerStatus, bandwidthLimits, - autoStreamProps, + streamConfig, }: TrackManagerConfig): TrackManager => { const [currentTrackId, setCurrentTrackId] = useState(null); const [paused, setPaused] = useState(false); @@ -49,7 +49,7 @@ export const useTrackManager = ({ const startStreaming = useCallback( async ( - props: StartStreamingProps = { simulcast: [Variant.VARIANT_LOW, Variant.VARIANT_MEDIUM, Variant.VARIANT_HIGH] }, + props: StreamConfig = { simulcast: [Variant.VARIANT_LOW, Variant.VARIANT_MEDIUM, Variant.VARIANT_HIGH] }, ) => { if (currentTrackId) throw Error("Track already added"); @@ -169,7 +169,7 @@ export const useTrackManager = ({ useEffect(() => { const onJoinedRoom = () => { if (mediaManager.getMedia()?.track) { - startStreaming(autoStreamProps); + startStreaming(streamConfig); } }; @@ -187,7 +187,7 @@ export const useTrackManager = ({ tsClient.off("joined", onJoinedRoom); tsClient.off("disconnected", onLeftRoom); }; - }, [mediaManager, startStreaming, tsClient, autoStreamProps, currentTrackId]); + }, [mediaManager, startStreaming, tsClient, streamConfig, currentTrackId]); return { currentTrack, diff --git a/packages/react-client/src/hooks/useConnection.ts b/packages/react-client/src/hooks/useConnection.ts index 64793706..4cd3819d 100644 --- a/packages/react-client/src/hooks/useConnection.ts +++ b/packages/react-client/src/hooks/useConnection.ts @@ -1,41 +1,64 @@ -import type { GenericMetadata } from "@fishjam-cloud/ts-client"; +import type { GenericMetadata, ReconnectionStatus } from "@fishjam-cloud/ts-client"; import { useCallback } from "react"; +import type { PeerStatus } from "../types/public"; import { useFishjamContext } from "./internal/useFishjamContext"; import { useReconnection } from "./internal/useReconnection"; export interface JoinRoomConfig { /** - * fishjam URL + * Fishjam URL */ url: string; /** - * token received from server (or Room Manager) + * Token received from server (or Room Manager) */ peerToken: string; /** - * string indexed record with metadata, that will be available to all other peers + * String indexed record with metadata, that will be available to all other peers */ peerMetadata?: PeerMetadata; } +/** + * @category Connection + */ +export interface UseConnectionResult { + /** + * Join room and start streaming camera and microphone + * + * @param {JoinRoomConfig} + */ + joinRoom: ( + config: JoinRoomConfig, + ) => Promise; + /** + * Leave room and stop streaming + */ + leaveRoom: () => void; + /** + * Current peer connection status + */ + peerStatus: PeerStatus; + /** + * Current reconnection status + */ + reconnectionStatus: ReconnectionStatus; +} + /** * Hook allows to to join or leave a room and check the current connection status. * @category Connection - * @returns + * @returns {UseConnectionResult} */ -export function useConnection() { +export function useConnection(): UseConnectionResult { const context = useFishjamContext(); const client = context.fishjamClientRef.current; const reconnectionStatus = useReconnection(); - const joinRoom = useCallback( - ({ - url, - peerToken, - peerMetadata, - }: JoinRoomConfig) => client.connect({ url, token: peerToken, peerMetadata: peerMetadata ?? {} }), + const joinRoom: UseConnectionResult["joinRoom"] = useCallback( + ({ url, peerToken, peerMetadata }) => client.connect({ url, token: peerToken, peerMetadata: peerMetadata ?? {} }), [client], ); @@ -46,17 +69,8 @@ export function useConnection() { const peerStatus = context.peerStatus; return { - /** - * Join room and start streaming camera and microphone - * - * See {@link JoinRoomConfig} for parameter list - */ joinRoom, - /** - * Leave room and stop streaming - */ leaveRoom, - peerStatus, reconnectionStatus, }; diff --git a/packages/react-client/src/hooks/usePeers.ts b/packages/react-client/src/hooks/usePeers.ts index edac0921..ea58db93 100644 --- a/packages/react-client/src/hooks/usePeers.ts +++ b/packages/react-client/src/hooks/usePeers.ts @@ -2,16 +2,31 @@ import type { Component, Endpoint, FishjamTrackContext, + Metadata, Peer, TrackContext, TrackMetadata, } from "@fishjam-cloud/ts-client"; -import type { DistinguishedTracks, PeerState } from "../types/internal"; +import type { PeerId } from "../types/internal"; import type { Track } from "../types/public"; import { useFishjamContext } from "./internal/useFishjamContext"; -export type PeerWithTracks = PeerState & DistinguishedTracks; +/** + * + * @category Connection + * @typeParam PeerMetadata Type of metadata set by peer while connecting to a room. + * @typeParam ServerMetadata Type of metadata set by the server while creating a peer. + */ +export type PeerWithTracks = { + id: PeerId; + metadata?: Metadata; + tracks: Track[]; + cameraTrack?: Track; + microphoneTrack?: Track; + screenShareVideoTrack?: Track; + screenShareAudioTrack?: Track; +}; function trackContextToTrack(track: FishjamTrackContext | TrackContext): Track { return { @@ -20,7 +35,6 @@ function trackContextToTrack(track: FishjamTrackContext | TrackContext): Track { stream: track.stream, simulcastConfig: track.simulcastConfig ?? null, encoding: track.encoding ?? null, - vadStatus: track.vadStatus, track: track.track, }; } @@ -45,41 +59,51 @@ function getPeerWithDistinguishedTracks(peer: Peer | Component | End } /** - * Result type for the usePeers hook. + * + * @category Connection + * @typeParam PeerMetadata Type of metadata set by peer while connecting to a room. + * @typeParam ServerMetadata Type of metadata set by the server while creating a peer. */ -export type UsePeersResult = { +export type UsePeersResult = { /** * The local peer with distinguished tracks (camera, microphone, screen share). * Will be null if the local peer is not found. */ - localPeer: PeerWithTracks | null; + localPeer: PeerWithTracks | null; /** * Array of remote peers with distinguished tracks (camera, microphone, screen share). */ - remotePeers: PeerWithTracks[]; + remotePeers: PeerWithTracks[]; /** * @deprecated Use remotePeers instead * Legacy array containing remote peers. * This property will be removed in future versions. */ - peers: PeerWithTracks[]; + peers: PeerWithTracks[]; }; /** * * @category Connection * @group Hooks - * @typeParam P Type of metadata set by peer while connecting to a room. - * @typeParam S Type of metadata set by the server while creating a peer. + * @typeParam PeerMetadata Type of metadata set by peer while connecting to a room. + * @typeParam ServerMetadata Type of metadata set by the server while creating a peer. */ -export function usePeers

, S = Record>(): UsePeersResult { +export function usePeers< + PeerMetadata = Record, + ServerMetadata = Record, +>(): UsePeersResult { const { clientState } = useFishjamContext(); - const localPeer = clientState.localPeer ? getPeerWithDistinguishedTracks(clientState.localPeer) : null; + const localPeer = clientState.localPeer + ? getPeerWithDistinguishedTracks(clientState.localPeer) + : null; - const remotePeers = Object.values(clientState.peers).map((peer) => getPeerWithDistinguishedTracks(peer)); + const remotePeers = Object.values(clientState.peers).map((peer) => + getPeerWithDistinguishedTracks(peer), + ); return { localPeer, remotePeers, peers: remotePeers }; } diff --git a/packages/react-client/src/hooks/useScreenShare.ts b/packages/react-client/src/hooks/useScreenShare.ts index 0eb9d212..6aa7036a 100644 --- a/packages/react-client/src/hooks/useScreenShare.ts +++ b/packages/react-client/src/hooks/useScreenShare.ts @@ -1,167 +1,29 @@ -import type { FishjamClient } from "@fishjam-cloud/ts-client"; -import { useCallback, useEffect, useRef, useState } from "react"; - -import type { ScreenShareState } from "../types/internal"; -import type { PeerStatus, ScreenshareApi, TracksMiddleware } from "../types/public"; -import { getRemoteOrLocalTrack } from "../utils/track"; +import type { Track, TracksMiddleware } from "../types/public"; import { useFishjamContext } from "./internal/useFishjamContext"; -interface ScreenShareManagerProps { - fishjamClient: FishjamClient; - getCurrentPeerStatus: () => PeerStatus; -} - -export const useScreenShareManager = ({ - fishjamClient, - getCurrentPeerStatus, -}: ScreenShareManagerProps): ScreenshareApi => { - const [state, setState] = useState({ stream: null, trackIds: null }); - - const cleanMiddlewareFnRef = useRef<(() => void) | null>(null); - - const stream = state.stream ?? null; - const [mediaVideoTrack, mediaAudioTrack] = stream ? getTracksFromStream(stream) : [null, null]; - - const getDisplayName = () => { - const name = fishjamClient.getLocalPeer()?.metadata?.peer?.displayName; - if (typeof name === "string") return name; - }; - - const startStreaming: ScreenshareApi["startStreaming"] = async (props) => { - const displayStream = await navigator.mediaDevices.getDisplayMedia({ - video: props?.videoConstraints ?? true, - audio: props?.audioConstraints ?? true, - }); - - const displayName = getDisplayName(); - - let [video, audio] = getTracksFromStream(displayStream); - - if (state.tracksMiddleware) { - const { videoTrack, audioTrack, onClear } = state.tracksMiddleware(video, audio); - video = videoTrack; - audio = audioTrack; - cleanMiddlewareFnRef.current = onClear; - } - - const addTrackPromises = [fishjamClient.addTrack(video, { displayName, type: "screenShareVideo", paused: false })]; - if (audio) - addTrackPromises.push(fishjamClient.addTrack(audio, { displayName, type: "screenShareAudio", paused: false })); - - const [videoId, audioId] = await Promise.all(addTrackPromises); - setState({ stream: displayStream, trackIds: { videoId, audioId } }); - }; - - const replaceTracks = async (newVideoTrack: MediaStreamTrack, newAudioTrack: MediaStreamTrack | null) => { - if (!state?.stream) return; - - const addTrackPromises = [fishjamClient.replaceTrack(state.trackIds.videoId, newVideoTrack)]; - - if (newAudioTrack && state.trackIds.audioId) { - addTrackPromises.push(fishjamClient.replaceTrack(state.trackIds.audioId, newAudioTrack)); - } - - await Promise.all(addTrackPromises); - }; - - const cleanMiddleware = useCallback(() => { - cleanMiddlewareFnRef.current?.(); - cleanMiddlewareFnRef.current = null; - }, []); - - const setTracksMiddleware = async (middleware: TracksMiddleware | null): Promise => { - if (!state?.stream) return; - - const [video, audio] = getTracksFromStream(state.stream); - - cleanMiddleware(); - - const { videoTrack, audioTrack, onClear } = middleware?.(video, audio) ?? { - videoTrack: video, - audioTrack: audio, - onClear: null, - }; - cleanMiddlewareFnRef.current = onClear; - await replaceTracks(videoTrack, audioTrack); - }; - - const stopStreaming: ScreenshareApi["stopStreaming"] = useCallback(async () => { - if (!state.stream) { - console.warn("No stream to stop"); - return; - } - const [video, audio] = getTracksFromStream(state.stream); - - video.stop(); - if (audio) audio.stop(); - - if (getCurrentPeerStatus() === "connected") { - const removeTrackPromises = [fishjamClient.removeTrack(state.trackIds.videoId)]; - if (state.trackIds.audioId) removeTrackPromises.push(fishjamClient.removeTrack(state.trackIds.audioId)); - - await Promise.all(removeTrackPromises); - } - - cleanMiddleware(); - setState(({ tracksMiddleware }) => ({ stream: null, trackIds: null, tracksMiddleware })); - }, [state, fishjamClient, setState, cleanMiddleware, getCurrentPeerStatus]); - - useEffect(() => { - if (!state.stream) return; - const [video, audio] = getTracksFromStream(state.stream); - - const trackEndedHandler = () => { - stopStreaming(); - }; - - video.addEventListener("ended", trackEndedHandler); - audio?.addEventListener("ended", trackEndedHandler); - - return () => { - video.removeEventListener("ended", trackEndedHandler); - audio?.removeEventListener("ended", trackEndedHandler); - }; - }, [state, stopStreaming]); - - useEffect(() => { - const onDisconnected = () => { - if (stream) { - stopStreaming(); - } - }; - fishjamClient.on("disconnected", onDisconnected); - - return () => { - fishjamClient.removeListener("disconnected", onDisconnected); - }; - }, [stopStreaming, fishjamClient, stream]); - - const videoBroadcast = state.stream ? getRemoteOrLocalTrack(fishjamClient, state.trackIds.videoId) : null; - const audioBroadcast = state.trackIds?.audioId ? getRemoteOrLocalTrack(fishjamClient, state.trackIds.audioId) : null; - - return { - startStreaming, - stopStreaming, - stream, - videoTrack: mediaVideoTrack, - audioTrack: mediaAudioTrack, - videoBroadcast, - audioBroadcast, - setTracksMiddleware, - currentTracksMiddleware: state?.tracksMiddleware ?? null, - }; -}; - -const getTracksFromStream = (stream: MediaStream): [MediaStreamTrack, MediaStreamTrack | null] => { - const video = stream.getVideoTracks()[0]; - const audio = stream.getAudioTracks()[0] ?? null; - - return [video, audio]; +/** + * + * @category Devices + * @group Types + */ +export type UseScreenshareResult = { + startStreaming: (props?: { + audioConstraints?: boolean | MediaTrackConstraints; + videoConstraints?: boolean | MediaTrackConstraints; + }) => Promise; + stopStreaming: () => Promise; + stream: MediaStream | null; + videoTrack: MediaStreamTrack | null; + audioTrack: MediaStreamTrack | null; + videoBroadcast: Track | null; + audioBroadcast: Track | null; + setTracksMiddleware: (middleware: TracksMiddleware | null) => Promise; + currentTracksMiddleware: TracksMiddleware | null; }; /** * - * @category Connection + * @category Devices * @group Hooks */ export const useScreenShare = () => { diff --git a/packages/react-client/src/hooks/useUpdatePeerMetadata.ts b/packages/react-client/src/hooks/useUpdatePeerMetadata.ts index 6a071216..e84c70be 100644 --- a/packages/react-client/src/hooks/useUpdatePeerMetadata.ts +++ b/packages/react-client/src/hooks/useUpdatePeerMetadata.ts @@ -4,7 +4,7 @@ import { useCallback } from "react"; import { useFishjamContext } from "./internal/useFishjamContext"; /** - * + * Hook provides a method to update the metadata of the local peer * @category Connection * @group Hooks * @returns diff --git a/packages/react-client/src/hooks/useVAD.ts b/packages/react-client/src/hooks/useVAD.ts index 03eb0438..e5673306 100644 --- a/packages/react-client/src/hooks/useVAD.ts +++ b/packages/react-client/src/hooks/useVAD.ts @@ -7,10 +7,10 @@ import { useFishjamContext } from "./internal/useFishjamContext"; /** * - * @param peerIds + * @param peerIds List of ids of peers to subscribe to for voice activity detection notifications. * @category Connection * @group Hooks - * @returns + * @returns Each key is a peerId and the boolean value indicates if voice activity is currently detected for that peer. */ export const useVAD = (peerIds: PeerId[]): Record => { const { fishjamClientRef } = useFishjamContext(); diff --git a/packages/react-client/src/index.ts b/packages/react-client/src/index.ts index 99582fd6..610a05f5 100644 --- a/packages/react-client/src/index.ts +++ b/packages/react-client/src/index.ts @@ -1,39 +1,28 @@ -export { FishjamProvider } from "./FishjamProvider"; -export { useCamera } from "./hooks/devices/useCamera"; -export { useInitializeDevices } from "./hooks/devices/useInitializeDevices"; -export { useMicrophone } from "./hooks/devices/useMicrophone"; -export type { JoinRoomConfig } from "./hooks/useConnection"; -export { useConnection } from "./hooks/useConnection"; -export type { PeerWithTracks } from "./hooks/usePeers"; -export { usePeers } from "./hooks/usePeers"; -export { useScreenShare } from "./hooks/useScreenShare"; +export { FishjamProvider, type FishjamProviderProps } from "./FishjamProvider"; +export { useCamera, type UseCameraResult } from "./hooks/devices/useCamera"; +export { useInitializeDevices, type UseInitializeDevicesResult } from "./hooks/devices/useInitializeDevices"; +export { useMicrophone, type UseMicrophoneResult } from "./hooks/devices/useMicrophone"; +export { type JoinRoomConfig, useConnection, type UseConnectionResult } from "./hooks/useConnection"; +export { type PeerWithTracks, usePeers, type UsePeersResult } from "./hooks/usePeers"; +export { useScreenShare, type UseScreenshareResult } from "./hooks/useScreenShare"; export { useUpdatePeerMetadata } from "./hooks/useUpdatePeerMetadata"; export { useVAD } from "./hooks/useVAD"; export type { - Device, DeviceItem, - DeviceType, PeerStatus, PersistLastDeviceHandlers, - ScreenshareApi, - StartStreamingProps, + StreamConfig, Track, TrackMiddleware, TracksMiddleware, } from "./types/public"; export type { AuthErrorReason, - BandwidthLimit, - CreateConfig, - EncodingReason, - MessageEvents, - Peer, + Metadata, + ReconnectConfig, ReconnectionStatus, SimulcastBandwidthLimit, SimulcastConfig, TrackBandwidthLimit, - TrackContext, - TrackContextEvents, - VadStatus, } from "@fishjam-cloud/ts-client"; export { Variant } from "@fishjam-cloud/ts-client"; diff --git a/packages/react-client/src/types/internal.ts b/packages/react-client/src/types/internal.ts index e071eeae..82e7a2b1 100644 --- a/packages/react-client/src/types/internal.ts +++ b/packages/react-client/src/types/internal.ts @@ -1,16 +1,8 @@ -import type { Peer } from "@fishjam-cloud/ts-client"; - import type { DeviceType, Track, TrackMiddleware, TracksMiddleware } from "./public"; export type TrackId = string; export type PeerId = string; -export type PeerState = { - id: PeerId; - metadata?: Peer["metadata"]; - tracks: Track[]; -}; - export type DevicesStatus = "OK" | "Error" | "Not requested" | "Requesting"; export type MediaStatus = "OK" | "Error" | "Not requested" | "Requesting"; @@ -89,10 +81,3 @@ export interface TrackManager { */ toggleDevice: () => Promise; } - -export type DistinguishedTracks = { - cameraTrack?: Track; - microphoneTrack?: Track; - screenShareVideoTrack?: Track; - screenShareAudioTrack?: Track; -}; diff --git a/packages/react-client/src/types/public.ts b/packages/react-client/src/types/public.ts index 120fe34c..730f89a5 100644 --- a/packages/react-client/src/types/public.ts +++ b/packages/react-client/src/types/public.ts @@ -1,4 +1,4 @@ -import type { SimulcastConfig, TrackMetadata, VadStatus, Variant } from "@fishjam-cloud/ts-client"; +import type { SimulcastConfig, TrackMetadata, Variant } from "@fishjam-cloud/ts-client"; import type { DeviceError, DeviceManagerStatus, TrackId } from "./internal"; @@ -8,7 +8,6 @@ export type Track = { trackId: TrackId; metadata?: TrackMetadata; simulcastConfig: SimulcastConfig | null; - vadStatus: VadStatus; track: MediaStreamTrack | null; }; @@ -47,28 +46,13 @@ export type PersistLastDeviceHandlers = { saveLastDevice: (info: MediaDeviceInfo) => void; }; -export type ScreenshareApi = { - startStreaming: (props?: { - audioConstraints?: boolean | MediaTrackConstraints; - videoConstraints?: boolean | MediaTrackConstraints; - }) => Promise; - stopStreaming: () => Promise; - stream: MediaStream | null; - videoTrack: MediaStreamTrack | null; - audioTrack: MediaStreamTrack | null; - videoBroadcast: Track | null; - audioBroadcast: Track | null; - setTracksMiddleware: (middleware: TracksMiddleware | null) => Promise; - currentTracksMiddleware: TracksMiddleware | null; -}; - export type SimulcastBandwidthLimits = { [Variant.VARIANT_LOW]: number; [Variant.VARIANT_MEDIUM]: number; [Variant.VARIANT_HIGH]: number; }; -export type StartStreamingProps = { simulcast?: Variant[] | false }; +export type StreamConfig = { simulcast?: Variant[] | false }; export type BandwidthLimits = { singleStream: number; simulcast: SimulcastBandwidthLimits }; diff --git a/packages/react-client/src/utils/track.ts b/packages/react-client/src/utils/track.ts index b2082e82..a6c93ae7 100644 --- a/packages/react-client/src/utils/track.ts +++ b/packages/react-client/src/utils/track.ts @@ -33,7 +33,6 @@ const getTrackFromContext = (context: TrackContext): Track => ({ stream: context.stream, simulcastConfig: context.simulcastConfig || null, encoding: context.encoding || null, - vadStatus: context.vadStatus, track: context.track, }); diff --git a/packages/ts-client/src/types.ts b/packages/ts-client/src/types.ts index 6c716724..923cbaf5 100644 --- a/packages/ts-client/src/types.ts +++ b/packages/ts-client/src/types.ts @@ -21,9 +21,15 @@ export type TrackMetadata = { export type GenericMetadata = Record | undefined; -export type Metadata

= { - peer: P; - server: S; +/** + * + * @category Connection + * @typeParam PeerMetadata Type of metadata set by peer while connecting to a room. + * @typeParam ServerMetadata Type of metadata set by the server while creating a peer. + */ +export type Metadata = { + peer: PeerMetadata; + server: ServerMetadata; }; type TrackContextEvents = {