Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 설정 다이얼로그를 통한 마이크/카메라 선택 기능 구현 #338

Merged
merged 11 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/src/assets/icons/setting.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 35 additions & 4 deletions apps/web/src/components/live/ControlBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import MicOnIc from '@/assets/icons/mic-on.svg?react';
import ScreenOffIc from '@/assets/icons/screen-off.svg?react';
import ScreenOnIc from '@/assets/icons/screen-on.svg?react';
import SettingIc from '@/assets/icons/setting.svg?react';
import ToggleButton from '@/components/live/ControlBar/ToggleButton';
import ExitDialog from '@/components/live/ExitDialog';
import SettingDialog from '@/components/live/SettingDialog';
import { useLocalStreamAction, useLocalStreamState } from '@/contexts/localStream/context';
import { useMediasoupAction, useMediasoupState } from '@/contexts/mediasoup/context';
import useModal from '@/hooks/useModal';
Expand All @@ -19,7 +21,17 @@
}

const ControlBar = ({ isOwner, onTicleEnd }: ControlBarProps) => {
const { isOpen, onClose, onOpen } = useModal();
const {
isOpen: isOpenExitModal,
onClose: onCloseExitModal,
onOpen: onOpenExitModal,
} = useModal();

const {
isOpen: isOpenSettingModal,
onClose: onCloseSettingModal,
onOpen: onOpenSettingModal,
} = useModal();

const { socketRef } = useMediasoupState();
const { video, screen, audio } = useLocalStreamState();
Expand All @@ -44,7 +56,7 @@
} else {
startScreenStream();
}
} catch (_) {

Check warning on line 59 in apps/web/src/components/live/ControlBar/index.tsx

View workflow job for this annotation

GitHub Actions / check

'_' is defined but never used
closeStream('screen');
}
};
Expand Down Expand Up @@ -91,6 +103,12 @@
return (
<>
<div className="flex items-center justify-start gap-x-[14px]">
<ToggleButton
isActivated
ActiveIcon={SettingIc}
InactiveIcon={SettingIc}
onToggle={onOpenSettingModal}
/>
<ToggleButton
ActiveIcon={MicOnIc}
InactiveIcon={MicOffIc}
Expand All @@ -109,10 +127,23 @@
onToggle={toggleScreenShare}
isActivated={screen.paused}
/>
<ToggleButton type="exit" ActiveIcon={ExitIc} InactiveIcon={ExitIc} onToggle={onOpen} />
<ToggleButton
type="exit"
ActiveIcon={ExitIc}
InactiveIcon={ExitIc}
onToggle={onOpenExitModal}
/>
</div>
{isOpen && (
<ExitDialog isOpen={isOpen} isOwner={isOwner} handleExit={handleExit} onClose={onClose} />
{isOpenExitModal && (
<ExitDialog
isOpen={isOpenExitModal}
isOwner={false}
handleExit={handleExit}
onClose={onCloseExitModal}
/>
)}
{isOpenSettingModal && (
<SettingDialog isOpen={isOpenSettingModal} onClose={onCloseSettingModal} />
)}
</>
);
Expand Down
82 changes: 82 additions & 0 deletions apps/web/src/components/live/SettingDialog/SelectMedia.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useEffect, useRef, useState } from 'react';

import { useLocalStreamAction, useLocalStreamState } from '@/contexts/localStream/context';
import { getCameraStream } from '@/utils/stream';

function SelectMedia() {
const videoRef = useRef<HTMLVideoElement>(null);
const [stream, setStream] = useState<MediaStream | null>(null);

const { videoDevices, audioDevices, selectedVideoDeviceId, selectedAudioDeviceId } =
useLocalStreamState();

const { setSelectedVideoDeviceId, setSelectedAudioDeviceId } = useLocalStreamAction();

useEffect(() => {
const getStream = async () => {
if (!selectedVideoDeviceId || !videoRef.current) return;

stream?.getTracks().forEach((track) => track.stop());

const cameraStream = await getCameraStream({ video: { deviceId: selectedVideoDeviceId } });

videoRef.current.srcObject = cameraStream;

setStream(cameraStream);
};

getStream();
}, [selectedVideoDeviceId]);

Check warning on line 29 in apps/web/src/components/live/SettingDialog/SelectMedia.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useEffect has a missing dependency: 'stream'. Either include it or remove the dependency array

return (
<div className="flex h-full flex-col gap-y-4 overflow-y-auto">
<div>
<h2 className="text-h4 text-alt">카메라</h2>
<video
ref={videoRef}
autoPlay
playsInline
className="aspect-video w-full rounded-lg bg-alt"
/>
{videoDevices.length === 0 && (
<p className="mt-2 text-body2 text-alt">사용 가능한 카메라가 없습니다.</p>
)}
{videoDevices.length !== 0 && (
<select
value={selectedVideoDeviceId ?? '선택'}
onChange={(e) => setSelectedVideoDeviceId(e.target.value)}
className="mt-2 w-full"
>
{videoDevices.map((device) => (
<option key={device.value} value={device.value}>
{device.label}
</option>
))}
</select>
Comment on lines +44 to +55
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4 기왕 하는거 Select 공통 컴포넌트를 사용하면 좋을거같긴합니다~! 우선순위는 매우 낮으니 이 부분은 제가 수정할게요!!

)}
</div>
<div>
<h2 className="text-h4 text-alt">마이크</h2>
{audioDevices.length === 0 && (
<p className="mt-2 text-body2 text-alt">사용 가능한 마이크가 없습니다.</p>
)}
{audioDevices.length !== 0 && (
<select
disabled={audioDevices.length === 0}
value={selectedAudioDeviceId ?? '선택'}
className="mt-2 w-full"
onChange={(e) => setSelectedAudioDeviceId(e.target.value)}
>
{audioDevices.map((device) => (
<option key={device.value} value={device.value}>
{device.label}
</option>
))}
</select>
)}
</div>
</div>
);
}

export default SelectMedia;
60 changes: 60 additions & 0 deletions apps/web/src/components/live/SettingDialog/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { cva } from 'class-variance-authority';
import { useState } from 'react';

import { Dialog } from '@/components/common/Dialog';
import SelectMedia from '@/components/live/SettingDialog/SelectMedia';

const listVariants = cva(
'flex w-full cursor-pointer items-center justify-center rounded-md px-4 py-2 text-sm',
{
variants: {
active: {
false: 'text-body1 hover:bg-alt',
true: 'bg-primary text-white',
},
},
defaultVariants: {
active: false,
},
}
);

const SIDEBAR_ITEMS = [
{
title: '오디오 및 비디오',
Component: SelectMedia,
},
] as const;

interface SettingDialogProps {
isOpen: boolean;
onClose: () => void;
}

function SettingDialog({ isOpen, onClose }: SettingDialogProps) {
const [activeIndex, setActiveIndex] = useState(0);

const Component = SIDEBAR_ITEMS[activeIndex]?.Component;

return (
<Dialog.Root isOpen={isOpen} onClose={onClose} className="flex h-[500px] w-[600px] flex-col">
<Dialog.Title>Settings</Dialog.Title>
<Dialog.Close onClose={onClose} />
<Dialog.Content className="flex h-full flex-1 items-center justify-center gap-x-4">
<ul className="flex h-full basis-32 flex-col items-start justify-start gap-y-2">
{SIDEBAR_ITEMS.map((item, index) => (
<li
key={index}
className={listVariants({ active: activeIndex === index })}
onClick={() => setActiveIndex(index)}
>
{item.title}
</li>
))}
</ul>
<div className="h-full flex-1">{Component && <Component />}</div>
</Dialog.Content>
</Dialog.Root>
);
}
export default SettingDialog;
44 changes: 22 additions & 22 deletions apps/web/src/components/live/StreamView/List/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,28 +53,6 @@ function VideoPlayer({

return (
<div className="relative flex h-full w-full cursor-pointer items-center justify-center overflow-hidden bg-darkAlt">
<div className="absolute bottom-3 left-3 z-10">
<Badge>{nickname}</Badge>
</div>
{!stream && (
<div
className={cn(
'flex h-full items-center justify-center',
videoVariants({ loading: false })
)}
>
<Loading />
</div>
)}
{stream && (
<>
{mediaType === 'video' && (
<div className="absolute right-3 top-3 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-altWeak p-1">
{isMicOn ? <MicOnIc className="text-white" /> : <MicOffIc className="fill-white" />}
</div>
)}
</>
)}
{stream &&
(!paused ? (
<video
Expand All @@ -98,6 +76,28 @@ function VideoPlayer({
/>
</div>
))}
{!stream && (
<div
className={cn(
'flex h-full items-center justify-center',
videoVariants({ loading: false })
)}
>
<Loading />
</div>
)}
<div className="absolute bottom-3 left-3">
<Badge>{nickname}</Badge>
</div>
{stream && (
<>
{mediaType === 'video' && (
<div className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full bg-altWeak p-1">
{isMicOn ? <MicOnIc className="text-white" /> : <MicOffIc className="fill-white" />}
</div>
)}
</>
)}
</div>
);
}
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/contexts/localStream/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,22 @@ export interface LocalStream {
paused: boolean;
}

export interface MediaDevice {
label: string;
value: string;
}

interface LocalStreamState {
audio: LocalStream;
video: LocalStream;
screen: LocalStream;

videoDevices: MediaDevice[];
audioDevices: MediaDevice[];
audioOutputDevices: MediaDevice[];
selectedVideoDeviceId: string | null;
selectedAudioDeviceId: string | null;
selectedAudioOutputDeviceId: string | null;
}

interface StreamActionContextProps {
Expand All @@ -21,6 +33,10 @@ interface StreamActionContextProps {
resumeStream: (type: MediaTypes) => void;
closeStream: (type: MediaTypes) => void;
closeLocalStream: () => void;

setSelectedVideoDeviceId: (deviceId: string) => void;
setSelectedAudioDeviceId: (deviceId: string) => void;
setSelectedAudioOutputDeviceId: (deviceId: string) => void;
}

export const LocalStreamStateContext = createContext<LocalStreamState | undefined>(undefined);
Expand Down
33 changes: 28 additions & 5 deletions apps/web/src/contexts/localStream/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,42 @@ export const LocalStreamProvider = ({ children }: StreamProviderProps) => {
resumeStream,
closeStream,
closeLocalStream,

videoDevices,
audioDevices,
audioOutputDevices,
selectedVideoDeviceId,
selectedAudioDeviceId,
selectedAudioOutputDeviceId,
setSelectedVideoDeviceId,
setSelectedAudioDeviceId,
setSelectedAudioOutputDeviceId,
} = useLocalStream();

const state = { video, audio, screen };
const state = {
video,
audio,
screen,
videoDevices,
audioDevices,
audioOutputDevices,
selectedVideoDeviceId,
selectedAudioDeviceId,
selectedAudioOutputDeviceId,
};

const actions = {
startCameraStream,
startMicStream,
startScreenStream,
closeStream,
pauseStream,
resumeStream,
closeStream,
startMicStream,
startCameraStream,
startScreenStream,
closeScreenStream,
closeLocalStream,
setSelectedVideoDeviceId,
setSelectedAudioDeviceId,
setSelectedAudioOutputDeviceId,
} as const;

return (
Expand Down
Loading
Loading