@selfcommunity/react-ui
Version:
React UI Components to integrate a Community created with SelfCommunity Platform.
295 lines (294 loc) • 17 kB
JavaScript
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}`] })] }))] }))] })));
}