@selfcommunity/react-ui
Version:
React UI Components to integrate a Community created with SelfCommunity Platform.
301 lines (300 loc) • 18.4 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.PreJoin = exports.usePreviewDevice = exports.usePreviewTracks = void 0;
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const livekit_client_1 = require("livekit-client");
const React = tslib_1.__importStar(require("react"));
const components_core_1 = require("@livekit/components-core");
const components_core_2 = require("@livekit/components-core");
const components_react_1 = require("@livekit/components-react");
const react_core_1 = require("@selfcommunity/react-core");
const ParticipantTileAvatar_1 = tslib_1.__importDefault(require("./ParticipantTileAvatar"));
const react_1 = require("react");
const TrackToggle_1 = require("./TrackToggle");
const LiveStreamProvider_1 = require("./LiveStreamProvider");
const track_processors_1 = require("@livekit/track-processors");
const LiveStreamSettingsMenu_1 = tslib_1.__importDefault(require("./LiveStreamSettingsMenu"));
const utils_1 = require("@selfcommunity/utils");
const LiveStream_1 = require("../../../constants/LiveStream");
const react_intl_1 = require("react-intl");
const notistack_1 = require("notistack");
/** @alpha */
function usePreviewTracks(options, onError) {
const [tracks, setTracks] = React.useState();
const [error, setError] = React.useState(false);
const trackLock = React.useMemo(() => new livekit_client_1.Mutex(), []);
React.useEffect(() => {
let needsCleanup = false;
let localTracks = [];
trackLock.lock().then((unlock) => tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
if (options.audio || options.video) {
localTracks = yield (0, livekit_client_1.createLocalTracks)(options);
if (needsCleanup) {
localTracks.forEach((tr) => tr.stop());
}
else {
setTracks(localTracks);
}
setError(false);
}
}
catch (e) {
if (onError && e instanceof Error) {
onError(e);
}
else {
components_core_1.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 };
}
exports.usePreviewTracks = usePreviewTracks;
/** @public */
function usePreviewDevice(enabled, deviceId, kind) {
const [deviceError, setDeviceError] = React.useState(null);
const [isCreatingTrack, setIsCreatingTrack] = React.useState(false);
const devices = (0, components_react_1.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) => tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
const track = kind === 'videoinput'
? yield (0, livekit_client_1.createLocalVideoTrack)({
deviceId: deviceId,
resolution: livekit_client_1.VideoPresets.h720.resolution
})
: yield (0, livekit_client_1.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) => tslib_1.__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) {
components_core_1.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) {
components_core_1.log.debug(`muting ${kind} track`);
localTrack.mute().then(() => components_core_1.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)) {
components_core_1.log.debug(`switching ${kind} device from`, prevDeviceId.current, selectedDevice.deviceId);
switchDevice(localTrack, selectedDevice.deviceId);
}
else {
components_core_1.log.debug(`unmuting local ${kind} track`);
localTrack.unmute();
}
}, [localTrack, selectedDevice, enabled, kind]);
React.useEffect(() => {
return () => {
if (localTrack) {
components_core_1.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
};
}
exports.usePreviewDevice = usePreviewDevice;
/**
* 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
*/
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 = tslib_1.__rest(_a, ["defaults", "onValidate", "onSubmit", "onError", "debug", "joinLabel", "micLabel", "camLabel", "userLabel", "persistUserChoices", "videoProcessor"]);
const { liveStream } = (0, LiveStreamProvider_1.useLiveStream)();
const scUserContext = (0, react_core_1.useSCUser)();
const [userChoices, setUserChoices] = React.useState(components_core_2.defaultUserChoices);
const canUseAudio = (0, react_1.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 = (0, react_1.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 } = (0, components_react_1.usePersistentUserChoices)({
defaults: partialDefaults,
preventSave: !persistUserChoices,
preventLoad: !persistUserChoices
});
const { enqueueSnackbar } = (0, notistack_1.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((0, utils_1.isClientSideRendering)() ? ((_b = window === null || window === void 0 ? void 0 : window.localStorage) === null || _b === void 0 ? void 0 : _b.getItem(LiveStream_1.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 === livekit_client_1.Track.Kind.Video)[0], [tracks]);
const facingMode = React.useMemo(() => {
if (videoTrack) {
const { facingMode } = (0, livekit_client_1.facingModeFromLocalTrack)(videoTrack);
return facingMode;
}
else {
return 'undefined';
}
}, [videoTrack]);
const audioTrack = React.useMemo(() => tracks === null || tracks === void 0 ? void 0 : tracks.filter((track) => track.kind === livekit_client_1.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(LiveStream_1.CHOICE_VIDEO_BLUR_EFFECT, _blur.toString());
}, [setBlurEnabled, blurEnabled]);
(0, react_1.useEffect)(() => {
const newUserChoices = {
username,
videoEnabled,
videoDeviceId,
audioEnabled,
audioDeviceId
};
setUserChoices(newUserChoices);
setIsValid(handleValidation(newUserChoices));
}, [username, scUserContext.user, videoEnabled, handleValidation, audioEnabled, audioDeviceId, videoDeviceId]);
(0, react_1.useEffect)(() => {
var _a;
if (videoTrack && videoEnabled) {
setProcessorPending(true);
try {
if (blurEnabled && !videoTrack.getProcessor()) {
videoTrack.setProcessor((0, track_processors_1.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(LiveStream_1.CHOICE_VIDEO_BLUR_EFFECT, false.toString());
enqueueSnackbar((0, jsx_runtime_1.jsx)(react_intl_1.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 {
components_core_1.log.warn('Validation failed with: ', userChoices);
}
}
return ((0, jsx_runtime_1.jsxs)("div", Object.assign({ className: "lk-prejoin" }, htmlProps, { children: [(0, jsx_runtime_1.jsxs)("div", Object.assign({ className: "lk-video-container" }, { children: [videoTrack && (0, jsx_runtime_1.jsx)("video", { ref: videoEl, width: "1280", height: "720", "data-lk-facing-mode": facingMode }), (!videoTrack || !videoEnabled) && ((0, jsx_runtime_1.jsx)("div", Object.assign({ className: "lk-camera-off-note" }, { children: (0, jsx_runtime_1.jsx)(ParticipantTileAvatar_1.default, { user: scUserContext.user }) })))] })), (0, jsx_runtime_1.jsxs)("div", Object.assign({ className: "lk-button-group-container" }, { children: [(0, jsx_runtime_1.jsxs)("div", Object.assign({ className: "lk-button-group audio" }, { children: [(0, jsx_runtime_1.jsx)(TrackToggle_1.TrackToggle, Object.assign({ disabled: !canUseAudio, initialState: audioEnabled, source: livekit_client_1.Track.Source.Microphone, onChange: (enabled) => setAudioEnabled(enabled) }, { children: micLabel })), (0, jsx_runtime_1.jsx)("div", Object.assign({ className: "lk-button-group-menu" }, { children: (0, jsx_runtime_1.jsx)(components_react_1.MediaDeviceMenu, { initialSelection: audioDeviceId, kind: "audioinput", disabled: !audioTrack || !canUseAudio || !audioEnabled, tracks: { audioinput: audioTrack }, onActiveDeviceChange: (_, id) => setAudioDeviceId(id) }) }))] })), (0, jsx_runtime_1.jsxs)("div", Object.assign({ className: "lk-button-group video" }, { children: [(0, jsx_runtime_1.jsx)(TrackToggle_1.TrackToggle, Object.assign({ disabled: !canUseVideo, initialState: videoEnabled, source: livekit_client_1.Track.Source.Camera, onChange: (enabled) => setVideoEnabled(enabled) }, { children: camLabel })), (0, jsx_runtime_1.jsx)("div", Object.assign({ className: "lk-button-group-menu" }, { children: (0, jsx_runtime_1.jsx)(components_react_1.MediaDeviceMenu, { initialSelection: videoDeviceId, kind: "videoinput", disabled: !videoTrack || !canUseVideo || !videoEnabled, tracks: { videoinput: videoTrack }, onActiveDeviceChange: (_, id) => setVideoDeviceId(id) }) }))] })), (0, jsx_runtime_1.jsx)(LiveStreamSettingsMenu_1.default, { actionBlurDisabled: !canUseVideo || !videoEnabled, blurEnabled: blurEnabled, handleBlur: handleBlur })] })), (0, jsx_runtime_1.jsx)("form", Object.assign({ className: "lk-username-container" }, { children: (0, jsx_runtime_1.jsx)("button", Object.assign({ className: "lk-button lk-join-button", type: "submit", onClick: handleSubmit, disabled: !isValid || error }, { children: joinLabel })) })), debug && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("strong", { children: "User Choices:" }), (0, jsx_runtime_1.jsxs)("ul", Object.assign({ className: "lk-list", style: { overflow: 'hidden', maxWidth: '15rem' } }, { children: [(0, jsx_runtime_1.jsxs)("li", { children: ["Username: ", `${userChoices.username}`] }), (0, jsx_runtime_1.jsxs)("li", { children: ["Video Enabled: ", `${userChoices.videoEnabled}`] }), (0, jsx_runtime_1.jsxs)("li", { children: ["Audio Enabled: ", `${userChoices.audioEnabled}`] }), (0, jsx_runtime_1.jsxs)("li", { children: ["Video Device: ", `${userChoices.videoDeviceId}`] }), (0, jsx_runtime_1.jsxs)("li", { children: ["Audio Device: ", `${userChoices.audioDeviceId}`] })] }))] }))] })));
}
exports.PreJoin = PreJoin;
;