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)