@fishjam-cloud/react-client
Version:
React client library for Fishjam
149 lines (148 loc) • 6.58 kB
JavaScript
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];
};