UNPKG

@fishjam-cloud/react-client

Version:
149 lines (148 loc) 6.58 kB
import { TrackTypeError } from "@fishjam-cloud/ts-client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useCurrentCallback } from "./useCurrentCallback"; export const useScreenShareManager = ({ fishjamClient, peerStatus, logger, }) => { const [state, setState] = useState({ stream: null, trackIds: null }); const cleanMiddlewareFnRef = useRef(null); const stream = state.stream ?? null; const tracksMiddleware = state.tracksMiddleware ?? null; const [mediaVideoTrack, mediaAudioTrack] = stream ? getTracksFromStream(stream) : [null, null]; const getDisplayName = useCallback(() => { const name = fishjamClient.getLocalPeer()?.metadata?.peer?.displayName; if (typeof name === "string") return name; }, [fishjamClient]); const addTrackToFishjamClient = useCallback(async (track, trackMetadata) => { try { return fishjamClient.addTrack(track, trackMetadata); } catch (err) { if (err instanceof TrackTypeError) { logger.warn(err.message); return undefined; } throw err; } }, [fishjamClient, logger]); const startStreaming = useCallback(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 (tracksMiddleware) { const { videoTrack, audioTrack, onClear } = await tracksMiddleware(video, audio); video = videoTrack; audio = audioTrack; cleanMiddlewareFnRef.current = onClear; } // TODO: FCE-2659 Refactor this hook so this check is not required. // This check is needed to support screensharing in livestreams which don't use the FishjamClient. // trackIds are simply ignored because they are not used in this use case. if (fishjamClient.status === "initialized") { const addTrackPromises = [ addTrackToFishjamClient(video, { displayName, type: "screenShareVideo", paused: false }), ]; if (audio) addTrackPromises.push(addTrackToFishjamClient(audio, { displayName, type: "screenShareAudio", paused: false })); const [videoId, audioId] = await Promise.all(addTrackPromises); setState({ stream: displayStream, trackIds: { videoId, audioId } }); } else { setState({ stream: displayStream, trackIds: {} }); } }, [tracksMiddleware, getDisplayName, addTrackToFishjamClient, fishjamClient]); const replaceTracks = useCallback(async (newVideoTrack, newAudioTrack) => { if (!state?.stream) return; const addTrackPromises = []; if (newVideoTrack && state.trackIds.videoId) addTrackPromises.push(fishjamClient.replaceTrack(state.trackIds.videoId, newVideoTrack)); if (newAudioTrack && state.trackIds.audioId) addTrackPromises.push(fishjamClient.replaceTrack(state.trackIds.audioId, newAudioTrack)); await Promise.all(addTrackPromises); }, [state.stream, state.trackIds?.videoId, state.trackIds?.audioId, fishjamClient]); const cleanMiddleware = useCallback(() => { cleanMiddlewareFnRef.current?.(); cleanMiddlewareFnRef.current = null; }, []); const setTracksMiddleware = useCallback(async (middleware) => { if (!state?.stream) return; const [video, audio] = getTracksFromStream(state.stream); cleanMiddleware(); const { videoTrack, audioTrack, onClear } = (await middleware?.(video, audio)) ?? { videoTrack: video, audioTrack: audio, onClear: null, }; cleanMiddlewareFnRef.current = onClear; await replaceTracks(videoTrack, audioTrack); }, [state.stream, cleanMiddleware, replaceTracks]); // Stable identity with a live closure: peerStatus must be observed at call // time so a captured reference doesn't skip the SFU removeTrack calls. const stopStreaming = useCurrentCallback(async () => { if (!state.stream) { logger.warn("No stream to stop"); return; } const [video, audio] = getTracksFromStream(state.stream); video.stop(); if (audio) audio.stop(); if (peerStatus === "connected") { const removeTrackPromises = []; if (state.trackIds.videoId) removeTrackPromises.push(fishjamClient.removeTrack(state.trackIds.videoId)); if (state.trackIds.audioId) removeTrackPromises.push(fishjamClient.removeTrack(state.trackIds.audioId)); await Promise.all(removeTrackPromises); } cleanMiddleware(); setState((prev) => ({ stream: null, trackIds: null, tracksMiddleware: prev.tracksMiddleware })); }); useEffect(() => { if (!state.stream) return; const [video, audio] = getTracksFromStream(state.stream); const trackEndedHandler = () => { void stopStreaming().catch((err) => { logger.error(err); }); }; video.addEventListener("ended", trackEndedHandler); audio?.addEventListener("ended", trackEndedHandler); return () => { video.removeEventListener("ended", trackEndedHandler); audio?.removeEventListener("ended", trackEndedHandler); }; }, [state, stopStreaming, logger]); useEffect(() => { const onDisconnected = () => { if (!stream) return; void stopStreaming().catch((err) => { logger.error(err); }); }; fishjamClient.on("disconnected", onDisconnected); return () => { fishjamClient.removeListener("disconnected", onDisconnected); }; }, [stopStreaming, fishjamClient, stream, logger]); return { startStreaming, stopStreaming, stream, videoTrack: mediaVideoTrack, audioTrack: mediaAudioTrack, setTracksMiddleware, currentTracksMiddleware: tracksMiddleware, }; }; const getTracksFromStream = (stream) => { const video = stream.getVideoTracks()[0]; const audio = stream.getAudioTracks()[0] ?? null; return [video, audio]; };