UNPKG

@selfcommunity/react-ui

Version:

React UI Components to integrate a Community created with SelfCommunity Platform.

295 lines (294 loc) • 17 kB
import { __awaiter, __rest } from "tslib"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { createLocalAudioTrack, createLocalTracks, createLocalVideoTrack, facingModeFromLocalTrack, Track, VideoPresets, Mutex } from 'livekit-client'; import * as React from 'react'; import { log } from '@livekit/components-core'; import { defaultUserChoices } from '@livekit/components-core'; import { MediaDeviceMenu, useMediaDevices, usePersistentUserChoices } from '@livekit/components-react'; import { useSCUser } from '@selfcommunity/react-core'; import ParticipantTileAvatar from './ParticipantTileAvatar'; import { useEffect, useMemo } from 'react'; import { TrackToggle } from './TrackToggle'; import { useLiveStream } from './LiveStreamProvider'; import { BackgroundBlur } from '@livekit/track-processors'; import LiveStreamSettingsMenu from './LiveStreamSettingsMenu'; import { isClientSideRendering } from '@selfcommunity/utils'; import { CHOICE_VIDEO_BLUR_EFFECT } from '../../../constants/LiveStream'; import { FormattedMessage } from 'react-intl'; import { useSnackbar } from 'notistack'; /** @alpha */ export function usePreviewTracks(options, onError) { const [tracks, setTracks] = React.useState(); const [error, setError] = React.useState(false); const trackLock = React.useMemo(() => new Mutex(), []); React.useEffect(() => { let needsCleanup = false; let localTracks = []; trackLock.lock().then((unlock) => __awaiter(this, void 0, void 0, function* () { try { if (options.audio || options.video) { localTracks = yield createLocalTracks(options); if (needsCleanup) { localTracks.forEach((tr) => tr.stop()); } else { setTracks(localTracks); } setError(false); } } catch (e) { if (onError && e instanceof Error) { onError(e); } else { log.error(e); } setError(true); } finally { unlock(); } })); return () => { needsCleanup = true; localTracks.forEach((track) => { track.stop(); }); setError(false); }; }, [JSON.stringify(options), onError, trackLock]); return { tracks, error }; } /** @public */ export function usePreviewDevice(enabled, deviceId, kind) { const [deviceError, setDeviceError] = React.useState(null); const [isCreatingTrack, setIsCreatingTrack] = React.useState(false); const devices = useMediaDevices({ kind }); const [selectedDevice, setSelectedDevice] = React.useState(undefined); const [localTrack, setLocalTrack] = React.useState(); const [localDeviceId, setLocalDeviceId] = React.useState(deviceId); React.useEffect(() => { setLocalDeviceId(deviceId); }, [deviceId]); const createTrack = (deviceId, kind) => __awaiter(this, void 0, void 0, function* () { try { const track = kind === 'videoinput' ? yield createLocalVideoTrack({ deviceId: deviceId, resolution: VideoPresets.h720.resolution }) : yield createLocalAudioTrack({ deviceId }); const newDeviceId = yield track.getDeviceId(); if (newDeviceId && deviceId !== newDeviceId) { prevDeviceId.current = newDeviceId; setLocalDeviceId(newDeviceId); } setLocalTrack(track); } catch (e) { if (e instanceof Error) { setDeviceError(e); } } }); const switchDevice = (track, id) => __awaiter(this, void 0, void 0, function* () { yield track.setDeviceId(id); prevDeviceId.current = id; }); const prevDeviceId = React.useRef(localDeviceId); React.useEffect(() => { if (enabled && !localTrack && !deviceError && !isCreatingTrack) { log.debug('creating track', kind); setIsCreatingTrack(true); createTrack(localDeviceId, kind).finally(() => { setIsCreatingTrack(false); }); } }, [enabled, localTrack, deviceError, isCreatingTrack]); // switch camera device React.useEffect(() => { if (!localTrack) { return; } if (!enabled) { log.debug(`muting ${kind} track`); localTrack.mute().then(() => log.debug(localTrack.mediaStreamTrack)); } else if ((selectedDevice === null || selectedDevice === void 0 ? void 0 : selectedDevice.deviceId) && prevDeviceId.current !== (selectedDevice === null || selectedDevice === void 0 ? void 0 : selectedDevice.deviceId)) { log.debug(`switching ${kind} device from`, prevDeviceId.current, selectedDevice.deviceId); switchDevice(localTrack, selectedDevice.deviceId); } else { log.debug(`unmuting local ${kind} track`); localTrack.unmute(); } }, [localTrack, selectedDevice, enabled, kind]); React.useEffect(() => { return () => { if (localTrack) { log.debug(`stopping local ${kind} track`); localTrack.stop(); localTrack.mute(); } }; }, []); React.useEffect(() => { setSelectedDevice(devices === null || devices === void 0 ? void 0 : devices.find((dev) => dev.deviceId === localDeviceId)); }, [localDeviceId, devices]); return { selectedDevice, localTrack, deviceError }; } /** * The `PreJoin` prefab component is normally presented to the user before he enters a room. * This component allows the user to check and select the preferred media device (camera und microphone). * On submit the user decisions are returned, which can then be passed on to the `LiveKitRoom` so that the user enters the room with the correct media devices. * * @remarks * This component is independent of the `LiveKitRoom` component and should not be nested within it. * Because it only accesses the local media tracks this component is self-contained and works without connection to the LiveKit server. * * @example * ```tsx * <PreJoin /> * ``` * @public */ export function PreJoin(_a) { var _b; var { defaults = {}, onValidate, onSubmit, onError, debug, joinLabel = 'Join Room', micLabel = 'Microphone', camLabel = 'Camera', userLabel = 'Username', persistUserChoices = true, videoProcessor } = _a, htmlProps = __rest(_a, ["defaults", "onValidate", "onSubmit", "onError", "debug", "joinLabel", "micLabel", "camLabel", "userLabel", "persistUserChoices", "videoProcessor"]); const { liveStream } = useLiveStream(); const scUserContext = useSCUser(); const [userChoices, setUserChoices] = React.useState(defaultUserChoices); const canUseAudio = useMemo(() => { var _a; return scUserContext.user && liveStream && (liveStream.host.id === scUserContext.user.id || (liveStream && !((_a = liveStream === null || liveStream === void 0 ? void 0 : liveStream.settings) === null || _a === void 0 ? void 0 : _a.muteParticipants))); }, [scUserContext, liveStream]); const canUseVideo = useMemo(() => { var _a; return scUserContext.user && liveStream && (liveStream.host.id === scUserContext.user.id || (liveStream && !((_a = liveStream === null || liveStream === void 0 ? void 0 : liveStream.settings) === null || _a === void 0 ? void 0 : _a.disableVideo))); }, [scUserContext, liveStream]); // TODO: Remove and pipe `defaults` object directly into `usePersistentUserChoices` once we fully switch from type `LocalUserChoices` to `UserChoices`. const partialDefaults = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (defaults.audioDeviceId !== undefined && { audioDeviceId: defaults.audioDeviceId })), (defaults.videoDeviceId !== undefined && { videoDeviceId: defaults.videoDeviceId })), (defaults.audioEnabled !== undefined && { audioEnabled: defaults.audioEnabled })), (defaults.videoEnabled !== undefined && { videoEnabled: defaults.videoEnabled })), (defaults.username !== undefined && { username: defaults.username })); const { userChoices: initialUserChoices, saveAudioInputDeviceId, saveAudioInputEnabled, saveVideoInputDeviceId, saveVideoInputEnabled, saveUsername } = usePersistentUserChoices({ defaults: partialDefaults, preventSave: !persistUserChoices, preventLoad: !persistUserChoices }); const { enqueueSnackbar } = useSnackbar(); // Initialize device settings const [audioEnabled, setAudioEnabled] = React.useState(initialUserChoices.audioEnabled && canUseAudio); const [videoEnabled, setVideoEnabled] = React.useState(initialUserChoices.videoEnabled && canUseVideo); const [audioDeviceId, setAudioDeviceId] = React.useState(initialUserChoices.audioDeviceId); const [videoDeviceId, setVideoDeviceId] = React.useState(initialUserChoices.videoDeviceId); const [username, setUsername] = React.useState(initialUserChoices.username); // Processors const [blurEnabled, setBlurEnabled] = React.useState(isClientSideRendering() ? ((_b = window === null || window === void 0 ? void 0 : window.localStorage) === null || _b === void 0 ? void 0 : _b.getItem(CHOICE_VIDEO_BLUR_EFFECT)) === 'true' : false); const [processorPending, setProcessorPending] = React.useState(false); // Save user choices to persistent storage. React.useEffect(() => { saveAudioInputEnabled(audioEnabled && canUseAudio); }, [audioEnabled, saveAudioInputEnabled, canUseAudio]); React.useEffect(() => { saveVideoInputEnabled(videoEnabled && canUseVideo); }, [videoEnabled, saveVideoInputEnabled, canUseVideo]); React.useEffect(() => { saveAudioInputDeviceId(audioDeviceId); }, [audioDeviceId, saveAudioInputDeviceId]); React.useEffect(() => { saveVideoInputDeviceId(videoDeviceId); }, [videoDeviceId, saveVideoInputDeviceId]); React.useEffect(() => { if (scUserContext.user) { saveUsername(scUserContext.user.username); } }, [username, saveUsername, scUserContext.user]); const { tracks, error } = usePreviewTracks({ audio: audioEnabled ? { deviceId: initialUserChoices.audioDeviceId } : false, video: videoEnabled ? { deviceId: initialUserChoices.videoDeviceId } : false }, onError); const videoEl = React.useRef(null); const videoTrack = React.useMemo(() => tracks === null || tracks === void 0 ? void 0 : tracks.filter((track) => track.kind === Track.Kind.Video)[0], [tracks]); const facingMode = React.useMemo(() => { if (videoTrack) { const { facingMode } = facingModeFromLocalTrack(videoTrack); return facingMode; } else { return 'undefined'; } }, [videoTrack]); const audioTrack = React.useMemo(() => tracks === null || tracks === void 0 ? void 0 : tracks.filter((track) => track.kind === Track.Kind.Audio)[0], [tracks]); React.useEffect(() => { if (videoEl.current && videoTrack) { videoTrack.unmute(); videoTrack.attach(videoEl.current); } return () => { videoTrack === null || videoTrack === void 0 ? void 0 : videoTrack.detach(); }; }, [videoTrack]); const [isValid, setIsValid] = React.useState(); const handleValidation = React.useCallback((values) => { if (typeof onValidate === 'function') { return onValidate(values); } else { return Boolean(values.username !== '' && ((values.audioEnabled && values.audioDeviceId) || !values.audioEnabled) && ((values.videoEnabled && values.videoDeviceId) || !values.videoEnabled)); } }, [onValidate]); const handleBlur = React.useCallback(() => { var _a; const _blur = !blurEnabled; setBlurEnabled(_blur); (_a = window === null || window === void 0 ? void 0 : window.localStorage) === null || _a === void 0 ? void 0 : _a.setItem(CHOICE_VIDEO_BLUR_EFFECT, _blur.toString()); }, [setBlurEnabled, blurEnabled]); useEffect(() => { const newUserChoices = { username, videoEnabled, videoDeviceId, audioEnabled, audioDeviceId }; setUserChoices(newUserChoices); setIsValid(handleValidation(newUserChoices)); }, [username, scUserContext.user, videoEnabled, handleValidation, audioEnabled, audioDeviceId, videoDeviceId]); useEffect(() => { var _a; if (videoTrack && videoEnabled) { setProcessorPending(true); try { if (blurEnabled && !videoTrack.getProcessor()) { videoTrack.setProcessor(BackgroundBlur(20)); } else if (!blurEnabled) { videoTrack.stopProcessor(); } } catch (e) { console.log(e); setBlurEnabled(false); (_a = window === null || window === void 0 ? void 0 : window.localStorage) === null || _a === void 0 ? void 0 : _a.setItem(CHOICE_VIDEO_BLUR_EFFECT, false.toString()); enqueueSnackbar(_jsx(FormattedMessage, { id: "ui.liveStreamRoom.errorApplyVideoEffect", defaultMessage: "ui.contributionActionMenu.errorApplyVideoEffect" }), { variant: 'warning', autoHideDuration: 3000 }); } finally { setProcessorPending(false); } } }, [blurEnabled, videoTrack, videoEnabled]); function handleSubmit(event) { event.preventDefault(); if (handleValidation(userChoices)) { if (typeof onSubmit === 'function') { onSubmit(userChoices); } } else { log.warn('Validation failed with: ', userChoices); } } return (_jsxs("div", Object.assign({ className: "lk-prejoin" }, htmlProps, { children: [_jsxs("div", Object.assign({ className: "lk-video-container" }, { children: [videoTrack && _jsx("video", { ref: videoEl, width: "1280", height: "720", "data-lk-facing-mode": facingMode }), (!videoTrack || !videoEnabled) && (_jsx("div", Object.assign({ className: "lk-camera-off-note" }, { children: _jsx(ParticipantTileAvatar, { user: scUserContext.user }) })))] })), _jsxs("div", Object.assign({ className: "lk-button-group-container" }, { children: [_jsxs("div", Object.assign({ className: "lk-button-group audio" }, { children: [_jsx(TrackToggle, Object.assign({ disabled: !canUseAudio, initialState: audioEnabled, source: Track.Source.Microphone, onChange: (enabled) => setAudioEnabled(enabled) }, { children: micLabel })), _jsx("div", Object.assign({ className: "lk-button-group-menu" }, { children: _jsx(MediaDeviceMenu, { initialSelection: audioDeviceId, kind: "audioinput", disabled: !audioTrack || !canUseAudio || !audioEnabled, tracks: { audioinput: audioTrack }, onActiveDeviceChange: (_, id) => setAudioDeviceId(id) }) }))] })), _jsxs("div", Object.assign({ className: "lk-button-group video" }, { children: [_jsx(TrackToggle, Object.assign({ disabled: !canUseVideo, initialState: videoEnabled, source: Track.Source.Camera, onChange: (enabled) => setVideoEnabled(enabled) }, { children: camLabel })), _jsx("div", Object.assign({ className: "lk-button-group-menu" }, { children: _jsx(MediaDeviceMenu, { initialSelection: videoDeviceId, kind: "videoinput", disabled: !videoTrack || !canUseVideo || !videoEnabled, tracks: { videoinput: videoTrack }, onActiveDeviceChange: (_, id) => setVideoDeviceId(id) }) }))] })), _jsx(LiveStreamSettingsMenu, { actionBlurDisabled: !canUseVideo || !videoEnabled, blurEnabled: blurEnabled, handleBlur: handleBlur })] })), _jsx("form", Object.assign({ className: "lk-username-container" }, { children: _jsx("button", Object.assign({ className: "lk-button lk-join-button", type: "submit", onClick: handleSubmit, disabled: !isValid || error }, { children: joinLabel })) })), debug && (_jsxs(_Fragment, { children: [_jsx("strong", { children: "User Choices:" }), _jsxs("ul", Object.assign({ className: "lk-list", style: { overflow: 'hidden', maxWidth: '15rem' } }, { children: [_jsxs("li", { children: ["Username: ", `${userChoices.username}`] }), _jsxs("li", { children: ["Video Enabled: ", `${userChoices.videoEnabled}`] }), _jsxs("li", { children: ["Audio Enabled: ", `${userChoices.audioEnabled}`] }), _jsxs("li", { children: ["Video Device: ", `${userChoices.videoDeviceId}`] }), _jsxs("li", { children: ["Audio Device: ", `${userChoices.audioDeviceId}`] })] }))] }))] }))); }