diff --git a/frontend/src/components/PreJoinPreview.tsx b/frontend/src/components/PreJoinPreview.tsx new file mode 100644 index 0000000..81c1608 --- /dev/null +++ b/frontend/src/components/PreJoinPreview.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useRef, useState } from "react"; + +const PreJoinPreview: React.FC<{ onJoin: (stream: MediaStream) => void }> = ({ onJoin }) => { + const videoRef = useRef(null); + const [stream, setStream] = useState(null); + const [devices, setDevices] = useState([]); + const [selectedCam, setSelectedCam] = useState(""); + const [selectedMic, setSelectedMic] = useState(""); + const [isMuted, setIsMuted] = useState(false); + const [isCamOn, setIsCamOn] = useState(true); + + // 🔹 Fetch devices + get default media + useEffect(() => { + const initMedia = async () => { + try { + const newStream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true, + }); + setStream(newStream); + if (videoRef.current) videoRef.current.srcObject = newStream; + + const devicesList = await navigator.mediaDevices.enumerateDevices(); + setDevices(devicesList); + } catch (err) { + console.error("Media access denied:", err); + } + }; + + initMedia(); + + return () => { + // 🔹 Cleanup + stream?.getTracks().forEach(track => track.stop()); + }; + }, []); + + const handleToggleMic = () => { + if (stream) { + stream.getAudioTracks().forEach(track => (track.enabled = !track.enabled)); + setIsMuted(prev => !prev); + } + }; + + const handleToggleCam = () => { + if (stream) { + stream.getVideoTracks().forEach(track => (track.enabled = !track.enabled)); + setIsCamOn(prev => !prev); + } + }; + + const handleDeviceChange = async (deviceId: string, type: "audioinput" | "videoinput") => { + if (!stream) return; + + // Stop old tracks of the same kind + stream.getTracks() + .filter(track => track.kind === (type === "audioinput" ? "audio" : "video")) + .forEach(track => track.stop()); + + const constraints: MediaStreamConstraints = + type === "audioinput" + ? { audio: { deviceId }, video: isCamOn } + : { video: { deviceId }, audio: true }; + + const newStream = await navigator.mediaDevices.getUserMedia(constraints); + setStream(newStream); + if (videoRef.current) videoRef.current.srcObject = newStream; + + if (type === "videoinput") setSelectedCam(deviceId); + else setSelectedMic(deviceId); + }; + + return ( +
+

Preview Your Setup

+ +
+ ); +}; + +export default PreJoinPreview; diff --git a/frontend/src/pages/InRoom.tsx b/frontend/src/pages/InRoom.tsx index 251919f..83325ad 100644 --- a/frontend/src/pages/InRoom.tsx +++ b/frontend/src/pages/InRoom.tsx @@ -1,237 +1,182 @@ import React, { useState, useEffect, useRef } from "react"; -import { Mic, MicOff, Video, VideoOff, PhoneOff, Users, MessageSquare } from "lucide-react"; +import { + Mic, + MicOff, + Video, + VideoOff, + PhoneOff, + Users, + MessageSquare, +} from "lucide-react"; import { motion } from "framer-motion"; -import { HotKeys } from "react-hotkeys"; - -const keyMap = { - TOGGLE_MIC: "ctrl+m", - TOGGLE_VIDEO: "ctrl+v", - TOGGLE_CHAT: "ctrl+c", -}; const InRoom: React.FC<{ roomName: string }> = ({ roomName }) => { - const [micOn, setMicOn] = useState(true); - const [videoOn, setVideoOn] = useState(true); - const [showChat, setShowChat] = useState(false); - const localVideoRef = useRef(null); - const remoteVideoRef = useRef(null); - const mediaStreamRef = useRef(null); - const [messages, setMessages] = useState<{ user: string; text: string; time?: string }[]>([]); - const [chatInput, setChatInput] = useState(""); - const chatEndRef = useRef(null); - - useEffect(() => { - document.title = `${roomName} | PeerCall`; - //initalize local media when joining the room - const initMedia = async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - video: true, + const [micOn, setMicOn] = useState(true); + const [videoOn, setVideoOn] = useState(true); + const [showChat, setShowChat] = useState(false); + + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + const [localStream, setLocalStream] = useState(null); + + // 🔹 STEP 1: Initialize camera and mic on join + useEffect(() => { + document.title = `${roomName} | PeerCall`; + + const initMedia = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true, + }); + setLocalStream(stream); + if (localVideoRef.current) { + localVideoRef.current.srcObject = stream; + } + } catch (err) { + console.error("Error accessing camera/mic:", err); + alert("Please allow camera and microphone permissions."); + } + }; + + initMedia(); + + // Cleanup when leaving room + return () => { + localStream?.getTracks().forEach((track) => track.stop()); + }; + }, [roomName]); + + // 🔹 STEP 2: Toggle mic + const handleToggleMic = () => { + if (!localStream) return; + localStream.getAudioTracks().forEach((track) => { + track.enabled = !track.enabled; }); - mediaStreamRef.current = stream; - if (localVideoRef.current) { - localVideoRef.current.srcObject = stream; - } - const hasAudio = stream.getAudioTracks().some((t) => t.enabled !== false); - const hasVideo = stream.getVideoTracks().some((t) => t.enabled !== false); - setMicOn(hasAudio); - setVideoOn(hasVideo); - } catch (err) { - console.error("Failed to get user media:", err);//due to denial or other unavailability issues - setMicOn(false); - setVideoOn(false); - } + setMicOn((prev) => !prev); }; - initMedia(); + // 🔹 STEP 3: Toggle video + const handleToggleVideo = () => { + if (!localStream) return; + localStream.getVideoTracks().forEach((track) => { + track.enabled = !track.enabled; + }); + setVideoOn((prev) => !prev); + }; - return () => { - if (mediaStreamRef.current) { - mediaStreamRef.current.getTracks().forEach((t) => t.stop()); - mediaStreamRef.current = null; - } + // 🔹 STEP 4: End call (for now just stop media) + const handleEndCall = () => { + localStream?.getTracks().forEach((track) => track.stop()); + window.location.href = "/"; // or navigate to dashboard/home }; - }, [roomName]); - - const handlers = {//to handle the keyboard events - TOGGLE_MIC: (e: KeyboardEvent) => { - e.preventDefault(); - toggleMic(); - }, - TOGGLE_VIDEO: (e: KeyboardEvent) => { - e.preventDefault(); - toggleVideo(); - }, - TOGGLE_CHAT: (e: KeyboardEvent) => { - e.preventDefault(); - toggleChat(); - }, - }; - const toggleMic = () => { - const stream = mediaStreamRef.current; - if (!stream) { - console.warn("No local media stream available to toggle mic"); - setMicOn((s) => !s); - return; - } - const audioTracks = stream.getAudioTracks(); - if (audioTracks.length === 0) { - console.warn("No audio tracks found"); - setMicOn(false); - return; - } - const enabled = !audioTracks[0].enabled; - audioTracks.forEach((t) => (t.enabled = enabled)); - setMicOn(enabled); - }; - - const toggleVideo = () => { - const stream = mediaStreamRef.current; - if (!stream) { - console.warn("No local media stream available to toggle video"); - setVideoOn((s) => !s); - return; - } - const videoTracks = stream.getVideoTracks(); - if (videoTracks.length === 0) { - console.warn("No video tracks found"); - setVideoOn(false); - return; - } - const enabled = !videoTracks[0].enabled; - videoTracks.forEach((t) => (t.enabled = enabled)); - setVideoOn(enabled); - }; - - const toggleChat = () => { - setShowChat((prev) => !prev); - }; - - useEffect(() => { - if(chatEndRef.current)chatEndRef.current.scrollIntoView({behavior:"smooth"}); - },[messages,showChat]); - - return ( - -
- {/* Header */} -
-

Room: {roomName}

-
- - 2 Participants -
-
- - {/* Main Video Area */} -
- {/* Video Grid */} -
-
-
-
-
-
- - {/* Chat Panel */} - {showChat && ( - -
- In-call Chat -
-
- {messages.length === 0 ? ( -

No messages yet...

- ) : ( -
- {messages.map((msg, idx) => ( -
-
- {msg.user} - {msg.time ? new Date(msg.time).toLocaleTimeString() : ""} + + return ( +
+ {/* Header */} +
+

Room: {roomName}

+
+ + 2 Participants +
+
+ + {/* Main Video Area */} +
+ {/* Video Grid */} +
+ {/* Local Video */} +
+
+ + {/* Remote Video */} +
+
+
+ + {/* Chat Panel (toggle) */} + {showChat && ( + +
+ In-call Chat +
+
+

No messages yet...

-
- {msg.text} +
+ +
-
- ))} -
-
+
)} -
-
{ e.preventDefault(); - if (chatInput.trim() === "") return; setMessages((s) => [...s, { user: "You", text: chatInput.trim(), time: new Date().toISOString() }]); setChatInput(""); - }}> - setChatInput(e.target.value)} - placeholder="Type a message..." - className="flex-1 bg-gray-800 rounded-l-md px-3 py-2 text-sm focus:outline-none" - /> - -
- - )} -
+
- {/* Controls */} -
- - - - - - - -
-
- - ); + {/* Controls */} +
+ + + + + + + +
+
+ ); }; export default InRoom; diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx index c780e5b..b2ffe45 100644 --- a/frontend/src/pages/Index.tsx +++ b/frontend/src/pages/Index.tsx @@ -1,9 +1,9 @@ -import Header from "../components/Header"; -import Hero from "../components/Hero"; -import Features from "../components/Features"; -import TechStack from "../components/TechStack"; -import Footer from "../components/Footer"; -import ChatOverlay from "../components/ChatOverlay"; +import Header from "../components/Header.js"; +import Hero from "../components/Hero.js"; +import Features from "../components/Features.js"; +import TechStack from "../components/TechStack.js"; +import Footer from "../components/Footer.js"; +import ChatOverlay from "../components/ChatOverlay.js"; import "../index.css"; const Index = () => { diff --git a/frontend/src/pages/JoinRoom.tsx b/frontend/src/pages/JoinRoom.tsx index a592e8d..e79009e 100644 --- a/frontend/src/pages/JoinRoom.tsx +++ b/frontend/src/pages/JoinRoom.tsx @@ -1,20 +1,31 @@ - import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button } from "../components/ui/button.js"; import axios from "axios"; +import PreJoinPreview from "../components/PreJoinPreview.js"; export default function JoinRoom() { const [roomName, setRoomName] = useState(""); const [loading, setLoading] = useState(false); + const [showPreview, setShowPreview] = useState(false); + const [stream, setStream] = useState(null); + const navigate = useNavigate(); - const API_BASE = "http://localhost:3000/api/rooms";// include /rooms here + const API_BASE = "http://localhost:3000/api/rooms"; - const handleJoin = async (e: React.FormEvent) => { + // 🔹 Step 1: Handle room join initiation + const handleJoinClick = (e: React.FormEvent) => { e.preventDefault(); if (!roomName.trim()) return alert("Please enter a room name!"); + // Instead of joining immediately, show preview first + setShowPreview(true); + }; + // 🔹 Step 2: After preview confirmation, actually join the backend room + const handleConfirmJoin = async (mediaStream: MediaStream) => { + setStream(mediaStream); setLoading(true); + try { const token = localStorage.getItem("token"); if (!token) throw new Error("No auth token found"); @@ -27,24 +38,36 @@ export default function JoinRoom() { } ); - // const roomName = res.data._id alert("Joined room successfully!"); + // Optional: Stop preview stream before entering actual call + mediaStream.getTracks().forEach(track => track.stop()); navigate(`/room/${roomName}`); } catch (err: any) { console.error("Join room error:", err); alert(err.response?.data?.message || err.message || "Failed to join room."); + setShowPreview(false); } finally { setLoading(false); } }; + // 🔹 Step 3: Conditional render — preview or join form + if (showPreview) { + return ( +
+ +
+ ); + } + + // 🔹 Step 4: Default room input form return (

Join a Room

-
+