Skip to content

Commit

Permalink
Merge pull request #216 from boostcampwm-2024/feature/#212
Browse files Browse the repository at this point in the history
[Feat] ์‚ฌ์šฉ์ž๋Š” ๋กœ๋น„ ํŽ˜์ด์ง€์—์„œ ๋ฏธ๋””์–ด ์žฅ์น˜๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.
  • Loading branch information
zizonyoungjun authored Dec 2, 2024
2 parents 925029c + 2f1585e commit 4855392
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 2 deletions.
3 changes: 3 additions & 0 deletions packages/frontend/src/assets/images/DropBoxArrowDown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/frontend/src/assets/images/DropBoxArrowUp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import DropBox from '@/components/lobbyPage/DropBox';
import { useLocalStreamStore } from '@/store/localStreamStore';

function MediaSourcePicker() {
return (
<div className="flex flex-col gap-4 text-black left-0 w-full">
<DropBox title={'์นด๋ฉ”๋ผ ์„ค์ •'} type="video" />
<DropBox title={'๋งˆ์ดํฌ ์„ค์ •'} type="audio" />
</div>
);
}

export default MediaSourcePicker;
87 changes: 87 additions & 0 deletions packages/frontend/src/components/lobbyPage/DropBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useState, useRef, useEffect } from 'react';
import ArrowDownIcon from '@/assets/images/DropBoxArrowDown.svg?react';
import ArrowUpIcon from '@/assets/images/DropBoxArrowUp.svg?react';
import { changeMediaDevice } from '@/utils/videoStreamUtils';
import { useLocalStreamStore } from '@/store/localStreamStore';

interface IDropBoxProps {
title: string;
type: 'video' | 'audio';
}

function DropBox({ title, type }: IDropBoxProps) {
const [isOpen, setIsOpen] = useState(false);
const dropBoxRef = useRef<HTMLDivElement>(null);
const videoDevices = useLocalStreamStore((state) => state.videoDevices);
const audioInputDevices = useLocalStreamStore((state) => state.audioInputDevices);
const currentVideoDevice = useLocalStreamStore((state) => state.currentVideoDevice);
const currentAudioDevice = useLocalStreamStore((state) => state.currentAudioDevice);
const targetMedia = type === 'video' ? currentVideoDevice : currentAudioDevice;
const targetMediaList = type === 'video' ? videoDevices : audioInputDevices;
const targetMediaLabel = targetMedia?.label || targetMediaList?.[0]?.label;

function handleClick() {
if (!targetMediaList?.length) return;
setIsOpen(!isOpen);
}

function handleDeviceChange(device: MediaDeviceInfo) {
if (device.kind === 'videoinput') {
changeMediaDevice({ videoDeviceId: device.deviceId, audioDeviceId: '' });
} else {
changeMediaDevice({ videoDeviceId: '', audioDeviceId: device.deviceId });
}
}

useEffect(() => {
function handleOutsideClick(event: MouseEvent) {
if (dropBoxRef.current && !dropBoxRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}

document.addEventListener('mousedown', handleOutsideClick);
return () => {
document.removeEventListener('mousedown', handleOutsideClick);
};
}, []);

return (
<div
ref={dropBoxRef}
className="flex flex-col gap-2 cursor-pointer select-none"
onClick={handleClick}
>
<span className="text-sm font-semibold">{title}</span>
<div className="flex items-center gap-2 relative">
<div className="w-full border border-black rounded-lg px-4 py-2 flex items-center justify-between">
{targetMediaList?.length ? (
targetMediaLabel
) : (
<span className="text-gray-500">์žฅ์น˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค</span>
)}
{isOpen ? <ArrowUpIcon /> : <ArrowDownIcon />}
</div>
{isOpen && (
<div className="border border-black rounded-lg p-2 absolute left-0 right-0 top-full mt-1 bg-white z-10">
{targetMediaList?.map((device) => {
if (device.deviceId.includes('communications')) return null;
return (
<div
key={device.deviceId}
className="p-2 hover:bg-gray-200 cursor-pointer rounded-lg flex items-center gap-2"
onClick={() => handleDeviceChange(device)}
>
{targetMediaLabel === device.label && <span className="text-green-500">โœ…</span>}
{device.label}
</div>
);
})}
</div>
)}
</div>
</div>
);
}

export default DropBox;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import VideoStream from '@/components/gamePage/stream/VideoStream';
import { useLocalStreamStore } from '@/store/localStreamStore';
import CameraSettingButton from '@/components/gamePage/stream/CameraSettingButton';
import MikeSettingButton from '@/components/gamePage/stream/MikeSettingButton';
import MediaSourcePicker from '@/components/gamePage/stream/MediaSourcePicker';

interface IVideoAudioTestModalProps {
onClose: () => void;
Expand All @@ -15,9 +16,14 @@ export default function VideoAudioTestModal({ onClose, title }: IVideoAudioTestM
<div className="min-w-[600px] max-w-lg p-8 bg-white shadow-lg rounded-2xl flex flex-col items-center gap-8">
<h2 className="mb-4 text-xl font-semibold text-center text-gray-900">{title}</h2>
<VideoStream userName="์นด๋ฉ”๋ผ ํ…Œ์ŠคํŠธ" stream={localStream} />
<MediaSourcePicker />
<div className="flex gap-4">
<CameraSettingButton iconColor="text-black" />
<MikeSettingButton iconColor="text-black" />
<div className="select-none">
<CameraSettingButton iconColor="text-black" />
</div>
<div className="select-none">
<MikeSettingButton iconColor="text-black" />
</div>
</div>
<button
onClick={onClose}
Expand Down
16 changes: 16 additions & 0 deletions packages/frontend/src/store/localStreamStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,26 @@ import { create } from 'zustand';

interface ILocalStreamState {
localStream: MediaStream | null;
videoDevices: MediaDeviceInfo[];
audioInputDevices: MediaDeviceInfo[];
currentVideoDevice: MediaDeviceInfo | null;
currentAudioDevice: MediaDeviceInfo | null;
setLocalStream: (stream: MediaStream | null) => void;
setVideoDevices: (devices: MediaDeviceInfo[]) => void;
setAudioInputDevices: (devices: MediaDeviceInfo[]) => void;
setCurrentVideoDevice: (device: MediaDeviceInfo | null) => void;
setCurrentAudioDevice: (device: MediaDeviceInfo | null) => void;
}

export const useLocalStreamStore = create<ILocalStreamState>((set) => ({
localStream: null,
videoDevices: [],
audioInputDevices: [],
currentVideoDevice: null,
currentAudioDevice: null,
setLocalStream: (stream) => set({ localStream: stream }),
setVideoDevices: (devices) => set({ videoDevices: devices }),
setAudioInputDevices: (devices) => set({ audioInputDevices: devices }),
setCurrentVideoDevice: (device) => set({ currentVideoDevice: device }),
setCurrentAudioDevice: (device) => set({ currentAudioDevice: device }),
}));
56 changes: 56 additions & 0 deletions packages/frontend/src/utils/videoStreamUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useLocalStreamStore } from '@/store/localStreamStore';

interface IMediaStreamProps {
videoDeviceId?: string;
audioDeviceId?: string;
}

export async function getVideoStream() {
const videoConfig = {
video: {
Expand All @@ -15,8 +20,59 @@ export async function getVideoStream() {
try {
const localStream = await navigator.mediaDevices.getUserMedia(videoConfig);
useLocalStreamStore.getState().setLocalStream(localStream);
await handleMediaDevices({ videoDeviceId: '', audioDeviceId: '' });
return localStream;
} catch (error) {
return null;
}
}

async function getTargetMediaDevices({ videoDeviceId, audioDeviceId }: IMediaStreamProps) {
const currentVideoDevice = useLocalStreamStore.getState().currentVideoDevice;
const currentAudioDevice = useLocalStreamStore.getState().currentAudioDevice;
const videoId = videoDeviceId || currentVideoDevice?.deviceId;
const audioId = audioDeviceId || currentAudioDevice?.deviceId;
const videoConfig = {
video: { deviceId: videoId },
audio: {
deviceId: audioId,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
};
try {
const localStream = await navigator.mediaDevices.getUserMedia(videoConfig);
await handleMediaDevices({ videoDeviceId, audioDeviceId });
return localStream;
} catch (error) {
return null;
}
}

async function handleMediaDevices({ videoDeviceId, audioDeviceId }: IMediaStreamProps) {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputDevices = devices.filter((device) => device.kind === 'audioinput');
const videoDevices = devices.filter((device) => device.kind === 'videoinput');
const currentVideoDevice = videoDeviceId
? videoDevices.find((device) => device.deviceId === videoDeviceId)
: null;
const currentAudioDevice = audioDeviceId
? audioInputDevices.find((device) => device.deviceId === audioDeviceId)
: null;
useLocalStreamStore.getState().setAudioInputDevices(audioInputDevices);
useLocalStreamStore.getState().setVideoDevices(videoDevices);
if (videoDeviceId)
useLocalStreamStore.getState().setCurrentVideoDevice(currentVideoDevice || null);
if (audioDeviceId)
useLocalStreamStore.getState().setCurrentAudioDevice(currentAudioDevice || null);
}

export async function changeMediaDevice({ videoDeviceId, audioDeviceId }: IMediaStreamProps) {
const localStream = useLocalStreamStore.getState().localStream;
const videoTrack = localStream?.getVideoTracks()[0];
if (videoTrack) videoTrack.stop();
const newStream = await getTargetMediaDevices({ videoDeviceId, audioDeviceId });
useLocalStreamStore.getState().setLocalStream(newStream);
await handleMediaDevices({ videoDeviceId, audioDeviceId });
}

0 comments on commit 4855392

Please sign in to comment.