diff --git a/packages/modules.calls/index.ts b/packages/modules.calls/index.ts
index 93ea958a3..56c22b389 100644
--- a/packages/modules.calls/index.ts
+++ b/packages/modules.calls/index.ts
@@ -1,6 +1,6 @@
export { Call, CompactView, useModeSync, ModeSyncProvider } from './src';
export { LiveKitProvider } from './src/providers/LiveKitProvider';
export { RoomProvider } from './src/providers/RoomProvider';
-export { useCallStore } from './src/store/callStore';
-export { useStartCall } from './src/hooks';
-export { useUmamiActivityHeartbeat } from './src';
+export { useCallStore, usePermissionsStore } from './src/store';
+export { useStartCall, usePersistentUserChoices } from './src/hooks';
+export { useUmamiActivityHeartbeat, MediaDeviceMenu, UserTile } from './src';
diff --git a/packages/modules.calls/src/index.ts b/packages/modules.calls/src/index.ts
index 3347ceb90..4463116bb 100644
--- a/packages/modules.calls/src/index.ts
+++ b/packages/modules.calls/src/index.ts
@@ -1,4 +1,4 @@
-export { Call, CompactView } from './ui';
+export { Call, CompactView, MediaDeviceMenu, UserTile } from './ui';
export { RoomProvider, LiveKitProvider, ModeSyncProvider } from './providers';
export {
useSize,
@@ -12,7 +12,8 @@ export {
useSpeakingParticipant,
useUmamiActivityHeartbeat,
useNoiseCancellation,
+ usePersistentUserChoices,
type UseNoiseCancellationOptions,
type UseNoiseCancellationResult,
} from './hooks';
-export { useCallStore } from './store';
+export { useCallStore, usePermissionsStore } from './store';
diff --git a/packages/modules.calls/src/store/index.ts b/packages/modules.calls/src/store/index.ts
index f5c596acc..8f573fb0a 100644
--- a/packages/modules.calls/src/store/index.ts
+++ b/packages/modules.calls/src/store/index.ts
@@ -1 +1,2 @@
export { useCallStore } from './callStore';
+export { usePermissionsStore } from './permissions';
diff --git a/packages/modules.calls/src/ui/PreJoin/components/MediaDevices/MediaDeviceMenu.tsx b/packages/modules.calls/src/ui/MediaDevices/MediaDeviceMenu.tsx
similarity index 100%
rename from packages/modules.calls/src/ui/PreJoin/components/MediaDevices/MediaDeviceMenu.tsx
rename to packages/modules.calls/src/ui/MediaDevices/MediaDeviceMenu.tsx
diff --git a/packages/modules.calls/src/ui/PreJoin/components/MediaDevices/MediaDeviceSelect.tsx b/packages/modules.calls/src/ui/MediaDevices/MediaDeviceSelect.tsx
similarity index 100%
rename from packages/modules.calls/src/ui/PreJoin/components/MediaDevices/MediaDeviceSelect.tsx
rename to packages/modules.calls/src/ui/MediaDevices/MediaDeviceSelect.tsx
diff --git a/packages/modules.calls/src/ui/PreJoin/components/MediaDevices/MediaDevices.tsx b/packages/modules.calls/src/ui/MediaDevices/MediaDevices.tsx
similarity index 94%
rename from packages/modules.calls/src/ui/PreJoin/components/MediaDevices/MediaDevices.tsx
rename to packages/modules.calls/src/ui/MediaDevices/MediaDevices.tsx
index 21d6898ad..1ea04cf8f 100644
--- a/packages/modules.calls/src/ui/PreJoin/components/MediaDevices/MediaDevices.tsx
+++ b/packages/modules.calls/src/ui/MediaDevices/MediaDevices.tsx
@@ -1,18 +1,18 @@
import { Button } from '@xipkg/button';
import { MediaDeviceMenu } from './MediaDeviceMenu';
-import { usePersistentUserChoices } from '../../../../hooks/usePersistentUserChoices';
+import { usePersistentUserChoices } from '../../hooks/usePersistentUserChoices';
import { useMemo } from 'react';
import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client';
-import { useCallStore } from '../../../../store/callStore';
-import { useRoom } from '../../../../providers/RoomProvider';
-import { usePermissionsStore } from '../../../../store/permissions';
+import { useCallStore } from '../../store/callStore';
+import { useRoom } from '../../providers/RoomProvider';
+import { usePermissionsStore } from '../../store/permissions';
import { Alert, AlertIcon, AlertContainer, AlertDescription } from '@xipkg/alert';
import { InfoCircle } from '@xipkg/icons';
import { Label } from '@xipkg/label';
import { Toggle } from '@xipkg/toggle';
import { supportsBackgroundProcessors } from '@livekit/track-processors';
-import type { UseNoiseCancellationResult } from '../../../../hooks/useNoiseCancellation';
-import { NoiseCancellationSettings } from '../../../../ui/shared/NoiseCancellationSettings';
+import type { UseNoiseCancellationResult } from '../../hooks/useNoiseCancellation';
+import { NoiseCancellationSettings } from '../shared/NoiseCancellationSettings';
interface MediaDevicesProps {
audioTrack?: LocalAudioTrack;
diff --git a/packages/modules.calls/src/ui/MediaDevices/index.ts b/packages/modules.calls/src/ui/MediaDevices/index.ts
new file mode 100644
index 000000000..4410d1b5e
--- /dev/null
+++ b/packages/modules.calls/src/ui/MediaDevices/index.ts
@@ -0,0 +1,2 @@
+export { MediaDevices } from './MediaDevices';
+export { MediaDeviceMenu } from './MediaDeviceMenu';
diff --git a/packages/modules.calls/src/ui/PreJoin/PreJoin.tsx b/packages/modules.calls/src/ui/PreJoin/PreJoin.tsx
index fb5872095..a864a8079 100644
--- a/packages/modules.calls/src/ui/PreJoin/PreJoin.tsx
+++ b/packages/modules.calls/src/ui/PreJoin/PreJoin.tsx
@@ -1,5 +1,7 @@
import { ScrollArea } from '@xipkg/scrollarea';
-import { Header, UserTile, MediaDevices } from './components';
+import { Header } from './components';
+import { UserTile } from '../UserTile';
+import { MediaDevices } from '../MediaDevices';
import { useMemo, useRef, useEffect, useCallback, useState } from 'react';
import {
Track,
diff --git a/packages/modules.calls/src/ui/PreJoin/components/MediaDevices/index.ts b/packages/modules.calls/src/ui/PreJoin/components/MediaDevices/index.ts
deleted file mode 100644
index 7d39ff9f0..000000000
--- a/packages/modules.calls/src/ui/PreJoin/components/MediaDevices/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { MediaDevices } from './MediaDevices';
diff --git a/packages/modules.calls/src/ui/PreJoin/components/index.ts b/packages/modules.calls/src/ui/PreJoin/components/index.ts
index 987c453b2..572b2d79d 100644
--- a/packages/modules.calls/src/ui/PreJoin/components/index.ts
+++ b/packages/modules.calls/src/ui/PreJoin/components/index.ts
@@ -1,3 +1 @@
export { Header } from './Header/Header';
-export { UserTile } from './UserTile/UserTile';
-export { MediaDevices } from './MediaDevices/MediaDevices';
diff --git a/packages/modules.calls/src/ui/PreJoin/components/UserTile/Controls.tsx b/packages/modules.calls/src/ui/UserTile/Controls.tsx
similarity index 84%
rename from packages/modules.calls/src/ui/PreJoin/components/UserTile/Controls.tsx
rename to packages/modules.calls/src/ui/UserTile/Controls.tsx
index c402e895f..477cd840a 100644
--- a/packages/modules.calls/src/ui/PreJoin/components/UserTile/Controls.tsx
+++ b/packages/modules.calls/src/ui/UserTile/Controls.tsx
@@ -1,15 +1,16 @@
import { LocalAudioTrack, LocalVideoTrack, Track } from 'livekit-client';
import { useCallback, useMemo } from 'react';
-import { DevicesBar } from '../../../shared/DevicesBar';
-import { usePersistentUserChoices } from '../../../../hooks/usePersistentUserChoices';
+import { DevicesBar } from '../shared/DevicesBar';
+import { usePersistentUserChoices } from '../../hooks/usePersistentUserChoices';
type ControlsProps = {
audioTrack?: LocalAudioTrack;
videoTrack?: LocalVideoTrack;
+ disableMicroToggle?: boolean;
};
-export const Controls = ({ audioTrack, videoTrack }: ControlsProps) => {
+export const Controls = ({ audioTrack, videoTrack, disableMicroToggle = false }: ControlsProps) => {
const {
userChoices: { audioEnabled, videoEnabled },
saveAudioInputEnabled,
@@ -83,11 +84,11 @@ export const Controls = ({ audioTrack, videoTrack }: ControlsProps) => {
);
return (
-
+
{
+ const { data: user } = useCurrentUser();
+ const { userId } = user;
+
+ const {
+ userChoices: { videoEnabled },
+ } = usePersistentUserChoices();
+
+ const videoEl = useRef(null);
+ const [isVideoInitiated, setIsVideoInitiated] = useState(false);
+
+ // Проверяем состояние разрешений
+ const isCameraDeniedOrPrompted = useCannotUseDevice('videoinput');
+ const isMicrophoneDeniedOrPrompted = useCannotUseDevice('audioinput');
+
+ const facingMode = useMemo(() => {
+ if (videoTrack) {
+ const { facingMode } = facingModeFromLocalTrack(videoTrack);
+ return facingMode;
+ }
+ return 'undefined';
+ }, [videoTrack]);
+
+ // Сбрасываем isVideoInitiated только при изменении videoEnabled на false
+ useEffect(() => {
+ if (!videoEnabled) {
+ console.log('UserTile: video disabled, setting isVideoInitiated to false');
+ setIsVideoInitiated(false);
+ }
+ // Не устанавливаем true здесь, так как это делается в обработчиках событий трека
+ }, [videoEnabled]);
+
+ // Отслеживаем изменение состояния muted трека
+ useEffect(() => {
+ if (videoTrack) {
+ const handleTrackMuted = () => {
+ console.log('UserTile: track muted, setting isVideoInitiated to false');
+ setIsVideoInitiated(false);
+ };
+
+ const handleTrackUnmuted = () => {
+ // Устанавливаем true только если videoEnabled тоже true
+ if (videoEnabled) {
+ console.log(
+ 'UserTile: track unmuted and video enabled, setting isVideoInitiated to true',
+ );
+ setIsVideoInitiated(true);
+ // Трек уже прикреплен к элементу, просто обновляем opacity через стили
+ } else {
+ console.log('UserTile: track unmuted but video disabled, keeping isVideoInitiated false');
+ }
+ };
+
+ videoTrack.on('muted', handleTrackMuted);
+ videoTrack.on('unmuted', handleTrackUnmuted);
+
+ return () => {
+ videoTrack.off('muted', handleTrackMuted);
+ videoTrack.off('unmuted', handleTrackUnmuted);
+ };
+ }
+ }, [videoTrack, videoEnabled]);
+
+ // Прикрепляем видео трек к элементу с улучшенной обработкой
+ useEffect(() => {
+ const currentVideoEl = videoEl.current;
+ const currentVideoTrack = videoTrack;
+
+ const handleVideoLoaded = () => {
+ if (currentVideoEl && videoEnabled) {
+ console.log('UserTile: video loaded and enabled, setting isVideoInitiated to true');
+ setIsVideoInitiated(true);
+ currentVideoEl.style.opacity = '1';
+ } else if (currentVideoEl) {
+ console.log('UserTile: video loaded but disabled, keeping isVideoInitiated false');
+ currentVideoEl.style.opacity = '0';
+ }
+ };
+
+ const handleVideoError = () => {
+ console.error('Video track error');
+ setIsVideoInitiated(false);
+ };
+
+ if (currentVideoEl && currentVideoTrack && videoEnabled) {
+ console.log('UserTile: attaching video track', {
+ videoEnabled,
+ isMuted: currentVideoTrack.isMuted,
+ hasElement: !!currentVideoEl,
+ });
+ currentVideoTrack.attach(currentVideoEl);
+ currentVideoEl.addEventListener('loadedmetadata', handleVideoLoaded);
+ currentVideoEl.addEventListener('error', handleVideoError);
+ } else {
+ console.log('UserTile: not attaching video track', {
+ hasElement: !!currentVideoEl,
+ hasTrack: !!currentVideoTrack,
+ videoEnabled,
+ isMuted: currentVideoTrack?.isMuted,
+ });
+ }
+
+ return () => {
+ if (currentVideoTrack) {
+ currentVideoTrack.detach();
+ }
+ if (currentVideoEl) {
+ currentVideoEl.removeEventListener('loadedmetadata', handleVideoLoaded);
+ currentVideoEl.removeEventListener('error', handleVideoError);
+ currentVideoEl.style.opacity = '0';
+ }
+ // Не сбрасываем isVideoInitiated здесь, так как это может происходить при переподключении
+ };
+ }, [videoTrack, videoEnabled]);
+
+ return (
+
+ );
+};
diff --git a/packages/modules.calls/src/ui/PreJoin/components/UserTile/UserTile.tsx b/packages/modules.calls/src/ui/UserTile/UserTileUI.tsx
similarity index 53%
rename from packages/modules.calls/src/ui/PreJoin/components/UserTile/UserTile.tsx
rename to packages/modules.calls/src/ui/UserTile/UserTileUI.tsx
index 4fa511a83..b316e4897 100644
--- a/packages/modules.calls/src/ui/PreJoin/components/UserTile/UserTile.tsx
+++ b/packages/modules.calls/src/ui/UserTile/UserTileUI.tsx
@@ -1,17 +1,14 @@
+import { useMemo } from 'react';
+import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client';
+import { isSafari } from '../../utils/livekit';
+import { openPermissionsDialog } from '../../store/permissions';
import { Avatar, AvatarFallback, AvatarImage } from '@xipkg/avatar';
-import { useMemo, useRef, useEffect, useState } from 'react';
-import { facingModeFromLocalTrack, LocalVideoTrack, LocalAudioTrack } from 'livekit-client';
-import { Controls } from './Controls';
-import { useCurrentUser } from 'common.services';
-import { usePersistentUserChoices } from '../../../../hooks/usePersistentUserChoices';
-import { useCannotUseDevice } from '../../../../hooks/useCannotUseDevice';
-import { openPermissionsDialog } from '../../../../store/permissions';
import { Button } from '@xipkg/button';
-import { SecureVideo } from '../../../shared';
import { Settings } from '@xipkg/icons';
-import { isSafari } from '../../../../utils/livekit';
+import { SecureVideo } from '../shared';
+import { Controls } from './Controls';
-const UserTileUI = ({
+export const UserTileUI = ({
audioTrack,
videoTrack,
videoEnabled,
@@ -21,6 +18,7 @@ const UserTileUI = ({
isCameraDeniedOrPrompted,
isMicrophoneDeniedOrPrompted,
isVideoInitiated,
+ disableMicroToggle = false,
}: {
audioTrack?: LocalAudioTrack;
videoTrack?: LocalVideoTrack;
@@ -31,6 +29,7 @@ const UserTileUI = ({
isCameraDeniedOrPrompted: boolean;
isMicrophoneDeniedOrPrompted: boolean;
isVideoInitiated: boolean;
+ disableMicroToggle?: boolean;
}) => {
const isPermissionsBlocked = isCameraDeniedOrPrompted || isMicrophoneDeniedOrPrompted;
@@ -176,143 +175,12 @@ const UserTileUI = ({
-
+
);
};
-
-interface UserTileProps {
- audioTrack?: LocalAudioTrack;
- videoTrack?: LocalVideoTrack;
-}
-
-export const UserTile = ({ audioTrack, videoTrack }: UserTileProps) => {
- const { data: user } = useCurrentUser();
- const { userId } = user;
-
- const {
- userChoices: { videoEnabled },
- } = usePersistentUserChoices();
-
- const videoEl = useRef(null);
- const [isVideoInitiated, setIsVideoInitiated] = useState(false);
-
- // Проверяем состояние разрешений
- const isCameraDeniedOrPrompted = useCannotUseDevice('videoinput');
- const isMicrophoneDeniedOrPrompted = useCannotUseDevice('audioinput');
-
- const facingMode = useMemo(() => {
- if (videoTrack) {
- const { facingMode } = facingModeFromLocalTrack(videoTrack);
- return facingMode;
- }
- return 'undefined';
- }, [videoTrack]);
-
- // Сбрасываем isVideoInitiated только при изменении videoEnabled на false
- useEffect(() => {
- if (!videoEnabled) {
- console.log('UserTile: video disabled, setting isVideoInitiated to false');
- setIsVideoInitiated(false);
- }
- // Не устанавливаем true здесь, так как это делается в обработчиках событий трека
- }, [videoEnabled]);
-
- // Отслеживаем изменение состояния muted трека
- useEffect(() => {
- if (videoTrack) {
- const handleTrackMuted = () => {
- console.log('UserTile: track muted, setting isVideoInitiated to false');
- setIsVideoInitiated(false);
- };
-
- const handleTrackUnmuted = () => {
- // Устанавливаем true только если videoEnabled тоже true
- if (videoEnabled) {
- console.log(
- 'UserTile: track unmuted and video enabled, setting isVideoInitiated to true',
- );
- setIsVideoInitiated(true);
- // Трек уже прикреплен к элементу, просто обновляем opacity через стили
- } else {
- console.log('UserTile: track unmuted but video disabled, keeping isVideoInitiated false');
- }
- };
-
- videoTrack.on('muted', handleTrackMuted);
- videoTrack.on('unmuted', handleTrackUnmuted);
-
- return () => {
- videoTrack.off('muted', handleTrackMuted);
- videoTrack.off('unmuted', handleTrackUnmuted);
- };
- }
- }, [videoTrack, videoEnabled]);
-
- // Прикрепляем видео трек к элементу с улучшенной обработкой
- useEffect(() => {
- const currentVideoEl = videoEl.current;
- const currentVideoTrack = videoTrack;
-
- const handleVideoLoaded = () => {
- if (currentVideoEl && videoEnabled) {
- console.log('UserTile: video loaded and enabled, setting isVideoInitiated to true');
- setIsVideoInitiated(true);
- currentVideoEl.style.opacity = '1';
- } else if (currentVideoEl) {
- console.log('UserTile: video loaded but disabled, keeping isVideoInitiated false');
- currentVideoEl.style.opacity = '0';
- }
- };
-
- const handleVideoError = () => {
- console.error('Video track error');
- setIsVideoInitiated(false);
- };
-
- if (currentVideoEl && currentVideoTrack && videoEnabled) {
- console.log('UserTile: attaching video track', {
- videoEnabled,
- isMuted: currentVideoTrack.isMuted,
- hasElement: !!currentVideoEl,
- });
- currentVideoTrack.attach(currentVideoEl);
- currentVideoEl.addEventListener('loadedmetadata', handleVideoLoaded);
- currentVideoEl.addEventListener('error', handleVideoError);
- } else {
- console.log('UserTile: not attaching video track', {
- hasElement: !!currentVideoEl,
- hasTrack: !!currentVideoTrack,
- videoEnabled,
- isMuted: currentVideoTrack?.isMuted,
- });
- }
-
- return () => {
- if (currentVideoTrack) {
- currentVideoTrack.detach();
- }
- if (currentVideoEl) {
- currentVideoEl.removeEventListener('loadedmetadata', handleVideoLoaded);
- currentVideoEl.removeEventListener('error', handleVideoError);
- currentVideoEl.style.opacity = '0';
- }
- // Не сбрасываем isVideoInitiated здесь, так как это может происходить при переподключении
- };
- }, [videoTrack, videoEnabled]);
-
- return (
-
- );
-};
diff --git a/packages/modules.calls/src/ui/PreJoin/components/UserTile/index.ts b/packages/modules.calls/src/ui/UserTile/index.ts
similarity index 100%
rename from packages/modules.calls/src/ui/PreJoin/components/UserTile/index.ts
rename to packages/modules.calls/src/ui/UserTile/index.ts
diff --git a/packages/modules.calls/src/ui/index.ts b/packages/modules.calls/src/ui/index.ts
index 1de871deb..3548b6b8b 100644
--- a/packages/modules.calls/src/ui/index.ts
+++ b/packages/modules.calls/src/ui/index.ts
@@ -4,3 +4,5 @@ export { ChatButton } from './Bottom/ChatButton';
export { Chat } from './Chat/Chat';
export { RaiseHandButton } from './Bottom/RaiseHandButton';
export { RaisedHandIndicator } from './Participant/RaisedHandIndicator';
+export { MediaDeviceMenu } from './MediaDevices';
+export { UserTile } from './UserTile';
diff --git a/packages/modules.profile/package.json b/packages/modules.profile/package.json
index 61bd54482..42dd8a4e4 100644
--- a/packages/modules.profile/package.json
+++ b/packages/modules.profile/package.json
@@ -11,6 +11,9 @@
"lint": "eslint ."
},
"dependencies": {
+ "@livekit/components-core": "0.12.9",
+ "@livekit/components-react": "2.9.14",
+ "@livekit/components-styles": "1.1.6",
"@xipkg/avatar": "3.3.0",
"@xipkg/badge": "2.0.12",
"@xipkg/button": "4.1.0",
@@ -32,6 +35,7 @@
"common.types": "workspace:*",
"dayjs": "1.11.13",
"features.avatar.editor": "workspace:*",
+ "modules.calls": "workspace:*",
"sonner": "^2.0.7"
},
"devDependencies": {
diff --git a/packages/modules.profile/src/ui/CameraPreview.tsx b/packages/modules.profile/src/ui/CameraPreview.tsx
new file mode 100644
index 000000000..b5c74e223
--- /dev/null
+++ b/packages/modules.profile/src/ui/CameraPreview.tsx
@@ -0,0 +1,71 @@
+import { createLocalVideoTrack, LocalAudioTrack, LocalVideoTrack } from 'livekit-client';
+import { MediaDeviceMenu } from '../../../modules.calls/src/ui/MediaDevices/MediaDeviceMenu';
+import { useEffect, useMemo, useState } from 'react';
+import { UserTile } from '../../../modules.calls/src/ui/UserTile/UserTile';
+import { usePersistentUserChoices } from '../../../modules.calls/src/hooks/usePersistentUserChoices';
+import { usePermissionsStore } from '../../../modules.calls/src/store/permissions';
+
+interface CameraPreviewProps {
+ audioTrack?: LocalAudioTrack;
+ videoTrack?: LocalVideoTrack;
+}
+
+export const CameraPreview = ({ audioTrack }: CameraPreviewProps) => {
+ const [videoTrack, setVideoTrack] = useState();
+ const {
+ userChoices: { videoDeviceId },
+ saveVideoInputEnabled,
+ saveVideoInputDeviceId,
+ } = usePersistentUserChoices();
+ const cameraPermission = usePermissionsStore((s) => s.cameraPermission);
+
+ const videoMenuKey = `videoinput-${cameraPermission}`;
+
+ useEffect(() => {
+ const initCamera = async () => {
+ try {
+ const track = await createLocalVideoTrack({
+ deviceId: { exact: videoDeviceId },
+ });
+ setVideoTrack(track);
+ } catch (error) {
+ console.error('Failed to start camera:', error);
+ }
+ };
+
+ initCamera();
+ }, []);
+
+ const handleVideoDeviceChange = useMemo(
+ () => async (_kind: MediaDeviceKind, deviceId: string) => {
+ try {
+ saveVideoInputDeviceId(deviceId);
+ if (videoTrack) {
+ await videoTrack.setDeviceId({ exact: deviceId });
+ // Синхронизируем состояние после смены устройства
+ const isActuallyEnabled = !videoTrack.isMuted;
+ saveVideoInputEnabled(isActuallyEnabled);
+ }
+ } catch (err) {
+ console.error('Failed to switch camera device', err);
+ }
+ },
+ [videoTrack, saveVideoInputDeviceId, saveVideoInputEnabled],
+ );
+
+ return (
+ <>
+
+
+
Камера
+
+
+ >
+ );
+};
diff --git a/packages/modules.profile/src/ui/CameraSettings.tsx b/packages/modules.profile/src/ui/CameraSettings.tsx
new file mode 100644
index 000000000..c73c549cd
--- /dev/null
+++ b/packages/modules.profile/src/ui/CameraSettings.tsx
@@ -0,0 +1,79 @@
+import { createLocalVideoTrack, LocalVideoTrack } from 'livekit-client';
+import { Camera } from '@xipkg/icons';
+import { ScrollArea } from '@xipkg/scrollarea';
+import { Category } from './Category';
+import { useMediaQuery } from '@xipkg/utils';
+import { MediaDeviceMenu } from 'modules.calls';
+import { UserTile } from 'modules.calls';
+import { useCallback, useEffect, useState } from 'react';
+import { usePersistentUserChoices } from 'modules.calls';
+import { usePermissionsStore } from 'modules.calls';
+
+export const CameraSettings = () => {
+ const isMobile = useMediaQuery('(max-width: 719px)');
+ const [videoTrack, setVideoTrack] = useState();
+ const {
+ userChoices: { videoDeviceId },
+ saveVideoInputEnabled,
+ saveVideoInputDeviceId,
+ } = usePersistentUserChoices();
+ const cameraPermission = usePermissionsStore((s) => s.cameraPermission);
+
+ const videoMenuKey = `videoinput-${cameraPermission}`;
+
+ useEffect(() => {
+ const initCamera = async () => {
+ try {
+ const track = await createLocalVideoTrack({
+ deviceId: { exact: videoDeviceId },
+ });
+ setVideoTrack(track);
+ } catch (error) {
+ console.error('Failed to start camera:', error);
+ }
+ };
+
+ initCamera();
+ }, [videoDeviceId]);
+
+ const handleVideoDeviceChange = useCallback(
+ async (_kind: MediaDeviceKind, deviceId: string) => {
+ try {
+ saveVideoInputDeviceId(deviceId);
+ if (videoTrack) {
+ await videoTrack.setDeviceId({ exact: deviceId });
+
+ const isActuallyEnabled = !videoTrack.isMuted;
+ saveVideoInputEnabled(isActuallyEnabled);
+ }
+ } catch (err) {
+ console.error('Failed to switch camera device', err);
+ }
+ },
+ [videoTrack, saveVideoInputDeviceId, saveVideoInputEnabled],
+ );
+
+ return (
+ <>
+ {!isMobile && (
+ Настройки видео
+ )}
+
+
+
} title="Камера">
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/packages/modules.profile/src/ui/Category.tsx b/packages/modules.profile/src/ui/Category.tsx
new file mode 100644
index 000000000..4380ce60d
--- /dev/null
+++ b/packages/modules.profile/src/ui/Category.tsx
@@ -0,0 +1,15 @@
+type CategoryProps = {
+ icon: React.ReactNode;
+ title: string;
+ children: React.ReactNode;
+};
+
+export const Category = ({ icon, title, children }: CategoryProps) => (
+
+
+ {icon}
+ {title}
+
+
{children}
+
+);
diff --git a/packages/modules.profile/src/ui/Content.tsx b/packages/modules.profile/src/ui/Content.tsx
index d8e96e6f4..0b758da16 100644
--- a/packages/modules.profile/src/ui/Content.tsx
+++ b/packages/modules.profile/src/ui/Content.tsx
@@ -5,6 +5,7 @@ import { PersonalData } from './PersonalData';
import { Notifications } from './Notifications';
import { Effects } from './Effects';
import { TechnicalReport } from './TechnicalReport';
+import { CameraSettings } from './CameraSettings';
type ComponentMapT = {
[key: string]: ReactElement;
@@ -17,6 +18,7 @@ const componentMap: ComponentMapT = {
notifications: ,
effects: ,
report: ,
+ cameraSettings: ,
};
type ContentPropsT = {
diff --git a/packages/modules.profile/src/ui/Effects.tsx b/packages/modules.profile/src/ui/Effects.tsx
index b9c38d847..44b88c643 100644
--- a/packages/modules.profile/src/ui/Effects.tsx
+++ b/packages/modules.profile/src/ui/Effects.tsx
@@ -4,6 +4,7 @@ import { Slider } from '@xipkg/slider';
import { Toggle } from '@xipkg/toggle';
import { useMediaQuery } from '@xipkg/utils';
import { useSoundEffectsStore, SOUND_DEFAULTS, type SoundKey } from 'common.ui';
+import { Category } from './Category';
type SoundItemProps = {
label: string;
@@ -92,22 +93,6 @@ const SoundItem = ({
);
};
-type CategoryProps = {
- icon: React.ReactNode;
- title: string;
- children: React.ReactNode;
-};
-
-const Category = ({ icon, title, children }: CategoryProps) => (
-
-
- {icon}
- {title}
-
-
{children}
-
-);
-
export const Effects = () => {
const isMobile = useMediaQuery('(max-width: 719px)');
diff --git a/packages/modules.profile/src/ui/Header.tsx b/packages/modules.profile/src/ui/Header.tsx
index 434cd1bcd..a20c612c1 100644
--- a/packages/modules.profile/src/ui/Header.tsx
+++ b/packages/modules.profile/src/ui/Header.tsx
@@ -9,6 +9,7 @@ const menuLabels = [
'Безопасность',
'Уведомления',
'Эффекты',
+ 'Настройки видео',
'Отчёт',
];
diff --git a/packages/modules.profile/src/ui/Menu.tsx b/packages/modules.profile/src/ui/Menu.tsx
index 5a4406849..40388d11a 100644
--- a/packages/modules.profile/src/ui/Menu.tsx
+++ b/packages/modules.profile/src/ui/Menu.tsx
@@ -1,4 +1,4 @@
-import { Account, Exit, Key, Palette, Notification, File, Music } from '@xipkg/icons';
+import { Account, Exit, Key, Palette, Notification, File, Music, Camera } from '@xipkg/icons';
import { Dispatch, SetStateAction } from 'react';
import { useLocation, useNavigate, useSearch } from '@tanstack/react-router';
import { useAuth } from 'common.auth';
@@ -29,6 +29,10 @@ const options: ItemT[] = [
name: 'Эффекты',
query: 'effects',
},
+ {
+ name: 'Настройки видео',
+ query: 'cameraSettings',
+ },
{
name: 'Отчёт',
query: 'report',
@@ -70,6 +74,8 @@ const Item = ({ index, item, onMenuItemChange }: ItemPropsT) => {
return ;
case 'report':
return ;
+ case 'cameraSettings':
+ return ;
default:
return null;
}
@@ -123,7 +129,7 @@ export const Menu = ({ setActiveContent, setActiveQuery, setShowContent }: MenuP
};
return (
-
+
{options.map((item, index) => (
))}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 656310047..93ffb8647 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3765,6 +3765,15 @@ importers:
packages/modules.profile:
dependencies:
+ '@livekit/components-core':
+ specifier: 0.12.9
+ version: 0.12.9(livekit-client@2.15.6(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
+ '@livekit/components-react':
+ specifier: 2.9.14
+ version: 2.9.14(@livekit/krisp-noise-filter@0.3.4(livekit-client@2.15.6(@types/dom-mediacapture-record@1.0.22)))(livekit-client@2.15.6(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)
+ '@livekit/components-styles':
+ specifier: 1.1.6
+ version: 1.1.6
'@xipkg/avatar':
specifier: 3.3.0
version: 3.3.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -3828,6 +3837,9 @@ importers:
features.avatar.editor:
specifier: workspace:*
version: link:../features.avatar.editor
+ modules.calls:
+ specifier: workspace:*
+ version: link:../modules.calls
react:
specifier: '19'
version: 19.2.5
@@ -9699,8 +9711,8 @@ packages:
peerDependencies:
eslint: '>=7.0.0'
- eslint-config-turbo@2.9.9:
- resolution: {integrity: sha512-KhO8N8ot1s9wXbkNDVgMwvkpbfO+YgEbteikFu406aQoJgenzNh5ErI985CeFKV3e33ZGLZX5YAJ1pgnhOXCZA==}
+ eslint-config-turbo@2.9.14:
+ resolution: {integrity: sha512-8bAXNDwtmHV7CuSDX+FB9+TslZEP8qJoNWY9FQTEhyO42bRimcExxwOh1+K2H2JV2VFXJrkt1KxyJmy2xm8Ukw==}
peerDependencies:
eslint: '>6.6.0'
turbo: '>2.0.0'
@@ -9790,14 +9802,14 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
- eslint-plugin-turbo@2.9.8:
- resolution: {integrity: sha512-czP2GjPzR6G+BpVEKXNa7Vs45/Zz0QjUEyB+SSLNpVJTqBe331AlE9IxkC2Eh00H6a1hf/Q6BVwf+1WJupyVYA==}
+ eslint-plugin-turbo@2.9.14:
+ resolution: {integrity: sha512-ROTlsO1JBJLATxtDNd7t22vviSb0hD8fKnjOO0WRgtxJW3VBRNO3BLAC129GPZSvEvtMH/f71Y2TzrqGPjLpEw==}
peerDependencies:
eslint: '>6.6.0'
turbo: '>2.0.0'
- eslint-plugin-turbo@2.9.9:
- resolution: {integrity: sha512-CHjV79PUXv5D+eCcnk3IMfu15X+EnwDSOWcVM72gQ3Ib8J+pF6zrY7A0kOgdS2HexdaGDoqzSLiXYRyu42TaGQ==}
+ eslint-plugin-turbo@2.9.8:
+ resolution: {integrity: sha512-czP2GjPzR6G+BpVEKXNa7Vs45/Zz0QjUEyB+SSLNpVJTqBe331AlE9IxkC2Eh00H6a1hf/Q6BVwf+1WJupyVYA==}
peerDependencies:
eslint: '>6.6.0'
turbo: '>2.0.0'
@@ -15671,10 +15683,10 @@ snapshots:
'@xipkg/eslint@3.2.0(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8)(typescript@5.7.3)':
dependencies:
'@typescript-eslint/parser': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3)
- eslint-config-airbnb: 19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react-hooks@5.1.0(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.2(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
+ eslint-config-airbnb: 19.0.4(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react-hooks@5.1.0(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.2(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
eslint-config-next: 15.1.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3)
eslint-config-prettier: 9.1.0(eslint@9.39.4(jiti@2.6.1))
- eslint-config-turbo: 2.9.9(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8)
+ eslint-config-turbo: 2.9.14(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react: 7.37.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react-hooks: 5.1.0(eslint@9.39.4(jiti@2.6.1))
@@ -16659,7 +16671,7 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)):
+ eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0)(eslint@9.39.4(jiti@2.6.1)):
dependencies:
confusing-browser-globals: 1.0.11
eslint: 9.39.4(jiti@2.6.1)
@@ -16668,10 +16680,10 @@ snapshots:
object.entries: 1.1.9
semver: 6.3.1
- eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react-hooks@5.1.0(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.2(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)):
+ eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react-hooks@5.1.0(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.2(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)):
dependencies:
eslint: 9.39.4(jiti@2.6.1)
- eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
+ eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react: 7.37.2(eslint@9.39.4(jiti@2.6.1))
@@ -16687,7 +16699,7 @@ snapshots:
'@typescript-eslint/parser': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3)
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1))
@@ -16707,10 +16719,10 @@ snapshots:
dependencies:
eslint: 9.39.4(jiti@2.6.1)
- eslint-config-turbo@2.9.9(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8):
+ eslint-config-turbo@2.9.14(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8):
dependencies:
eslint: 9.39.4(jiti@2.6.1)
- eslint-plugin-turbo: 2.9.9(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8)
+ eslint-plugin-turbo: 2.9.14(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8)
turbo: 2.9.8
eslint-import-resolver-node@0.3.10:
@@ -16721,7 +16733,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)):
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.39.4(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -16736,14 +16748,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3)
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.39.4(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@@ -16758,7 +16770,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
hasown: 2.0.3
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -16853,13 +16865,13 @@ snapshots:
string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0
- eslint-plugin-turbo@2.9.8(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8):
+ eslint-plugin-turbo@2.9.14(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8):
dependencies:
dotenv: 16.0.3
eslint: 9.39.4(jiti@2.6.1)
turbo: 2.9.8
- eslint-plugin-turbo@2.9.9(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8):
+ eslint-plugin-turbo@2.9.8(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.8):
dependencies:
dotenv: 16.0.3
eslint: 9.39.4(jiti@2.6.1)