diff --git a/frontend/src/components/session/MediaPreviewModal.tsx b/frontend/src/components/session/MediaPreviewModal.tsx index 2960d3f..615e5c6 100644 --- a/frontend/src/components/session/MediaPreviewModal.tsx +++ b/frontend/src/components/session/MediaPreviewModal.tsx @@ -38,9 +38,7 @@ const MediaPreviewModal = ({ const getMediaPreview = useCallback(async () => { const mediaStream = await getMediaStream("video"); if (!mediaStream) { - toast.error( - "비디오 장치를 찾을 수 없습니다. 비디오 장치 없이 세션에 참가합니다." - ); + toast.error("비디오 장치를 찾을 수 없습니다."); } setPreview(mediaStream); }, []); @@ -100,7 +98,7 @@ const MediaPreviewModal = ({ className={"w-6 h-6"} type={"checkbox"} title={"dd"} - onClick={() => setIsVideoOn(!isVideoOn)} + onChange={() => setIsVideoOn(!isVideoOn)} /> 내 비디오 끄고 참가하기 @@ -134,6 +132,7 @@ const MediaPreviewModal = ({ onConfirm(); setReady(true); modal.closeModal(); + preview?.getTracks().forEach((track) => track.stop()); }} className={ "rounded-custom-m px-16 py-4 bg-green-500 text-white hover:bg-green-600" diff --git a/frontend/src/pages/SessionPage/hooks/useBlockNavigate.ts b/frontend/src/pages/SessionPage/hooks/useBlockNavigate.ts index 3cb942e..41e54e2 100644 --- a/frontend/src/pages/SessionPage/hooks/useBlockNavigate.ts +++ b/frontend/src/pages/SessionPage/hooks/useBlockNavigate.ts @@ -29,6 +29,12 @@ const useBlockNavigate = () => { } }, [blocker]); + useEffect(() => { + return () => { + if (blocker) setShouldBlock(false); + }; + }, []); + useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { e.preventDefault(); diff --git a/frontend/src/pages/SessionPage/hooks/useMediaDevices.ts b/frontend/src/pages/SessionPage/hooks/useMediaDevices.ts index f7514b2..d02fe90 100644 --- a/frontend/src/pages/SessionPage/hooks/useMediaDevices.ts +++ b/frontend/src/pages/SessionPage/hooks/useMediaDevices.ts @@ -74,7 +74,7 @@ const useMediaDevices = (dataChannels: DataChannels) => { }; }, []); - const getMedia = async (videoOn = true) => { + const getMedia = async () => { try { if (streamRef.current) { // 이미 스트림이 있으면 종료 @@ -90,14 +90,14 @@ const useMediaDevices = (dataChannels: DataChannels) => { let audioStream = null; try { - videoStream = await navigator.mediaDevices.getUserMedia({ - video: videoOn - ? selectedVideoDeviceId - ? { deviceId: selectedVideoDeviceId } - : true - : false, - audio: false, - }); + videoStream = isVideoOn + ? await navigator.mediaDevices.getUserMedia({ + video: selectedVideoDeviceId + ? { deviceId: selectedVideoDeviceId } + : true, + audio: false, + }) + : null; } catch (videoError) { console.warn("비디오 스트림을 가져오는데 실패했습니다:", videoError); setIsVideoOn(false); @@ -120,7 +120,7 @@ const useMediaDevices = (dataChannels: DataChannels) => { // 스트림 병합 또는 개별 스트림 사용 let combinedStream = null; const tracks = [ - ...(videoStream?.getVideoTracks() || []), + ...(videoStream?.getVideoTracks() || [createDummyStream()]), ...(audioStream?.getAudioTracks() || []), ]; @@ -139,7 +139,6 @@ const useMediaDevices = (dataChannels: DataChannels) => { "미디어 스트림을 가져오는 도중 문제가 발생했습니다.", error ); - // 에러 처리 로직 (예: 사용자에게 알림) } finally { setVideoLoading(false); } @@ -182,20 +181,10 @@ const useMediaDevices = (dataChannels: DataChannels) => { if (isVideoOn) { for (const videoTrack of stream.getVideoTracks()) { // 비디오 끄기 - if (!videoTrack.enabled) { - setIsVideoOn((prev) => !prev); - return; - } - videoTrack.stop(); - const blackCanvas = document.createElement("canvas"); - blackCanvas.width = 640; - blackCanvas.height = 480; - const ctx = blackCanvas.getContext("2d"); - ctx!.fillRect(0, 0, blackCanvas.width, blackCanvas.height); + videoTrack.stop(); - const blackStream = blackCanvas.captureStream(); - const blackTrack = blackStream.getVideoTracks()[0]; + const blackTrack = createDummyStream(); Object.values(peerConnections || {}).forEach((pc) => { const sender = pc .getSenders() @@ -219,18 +208,22 @@ const useMediaDevices = (dataChannels: DataChannels) => { if (videoStream) { if (streamRef.current) { const oldVideoTracks = streamRef.current.getVideoTracks(); - oldVideoTracks.forEach((track) => - streamRef.current?.removeTrack(track) - ); + oldVideoTracks.forEach((track) => { + track.stop(); + streamRef.current?.removeTrack(track); + }); streamRef.current.addTrack(newVideoTrack); setStream(streamRef.current); + console.log("피어업데이트", peerConnections); Object.values(peerConnections || {}).forEach((pc) => { const sender = pc .getSenders() - .find((s) => s.track?.kind === "video"); + .find((s) => s.track!.kind === "video"); if (sender) { + console.log("비디오 켜기 업데이트"); + sender.replaceTrack(newVideoTrack); } }); @@ -284,6 +277,22 @@ const useMediaDevices = (dataChannels: DataChannels) => { } }; + const createDummyStream = () => { + const blackCanvas = document.createElement("canvas"); + blackCanvas.width = 640; + blackCanvas.height = 480; + const ctx = blackCanvas.getContext("2d"); + ctx!.fillRect(0, 0, blackCanvas.width, blackCanvas.height); + + const blackStream = blackCanvas.captureStream(); + const blackTrack = blackStream.getVideoTracks()[0]; + Object.defineProperty(blackTrack, "label", { + value: "blackTrack", + writable: false, + }); + return blackTrack; + }; + return { userAudioDevices, userVideoDevices, diff --git a/frontend/src/pages/SessionPage/hooks/usePeerConnection.ts b/frontend/src/pages/SessionPage/hooks/usePeerConnection.ts index c2a091d..fcbffdf 100644 --- a/frontend/src/pages/SessionPage/hooks/usePeerConnection.ts +++ b/frontend/src/pages/SessionPage/hooks/usePeerConnection.ts @@ -83,6 +83,25 @@ const usePeerConnection = (socket: Socket) => { mediaDataChannel.onopen = () => { console.log("Media data channel opened."); dataChannels.current[peerSocketId] = mediaDataChannel; + + const audioTracks = stream.getAudioTracks(); + const audioEnabled = audioTracks.length > 0 && audioTracks[0].enabled; + + const videoTracks = stream.getVideoTracks(); + const videoEnabled = + videoTracks.length > 0 && videoTracks[0].label !== "blackTrack"; + mediaDataChannel.send( + JSON.stringify({ + type: "audio", + status: audioEnabled, + }) + ); + mediaDataChannel.send( + JSON.stringify({ + type: "video", + status: videoEnabled, + }) + ); }; mediaDataChannel.onclose = () => { @@ -178,12 +197,20 @@ const usePeerConnection = (socket: Socket) => { }, ]; }); + + const audioTracks = e.streams[0].getAudioTracks(); + const audioEnabled = audioTracks.length > 0 && audioTracks[0].enabled; + + const videoTracks = e.streams[0].getVideoTracks(); + const videoEnabled = + videoTracks.length > 0 && videoTracks[0].label !== "blackTrack"; + setPeerMediaStatus((prev) => { return { ...prev, [peerSocketId]: { - audio: e.streams[0].getAudioTracks().length > 0, - video: e.streams[0].getVideoTracks().length > 0, + audio: audioEnabled, + video: videoEnabled, }, }; }); diff --git a/frontend/src/pages/SessionPage/hooks/useSession.ts b/frontend/src/pages/SessionPage/hooks/useSession.ts index f4a35f3..8cb0dcb 100644 --- a/frontend/src/pages/SessionPage/hooks/useSession.ts +++ b/frontend/src/pages/SessionPage/hooks/useSession.ts @@ -122,7 +122,7 @@ export const useSession = (sessionId: string) => { return; } - const mediaStream = await getMedia(isVideoOn); + const mediaStream = await getMedia(); if (!mediaStream) { toast.error( "미디어 스트림을 가져오지 못했습니다. 미디어 장치를 확인 후 다시 시도해주세요."