From 897b47d3c9ffbe72bbc79c7f3918e8b3349f44ae Mon Sep 17 00:00:00 2001 From: ADARSHsri2004 Date: Fri, 7 Nov 2025 16:35:20 +0530 Subject: [PATCH 1/2] feat: added audio and video preview --- frontend/src/components/PreJoinPreview.tsx | 136 +++++++++++++++++++++ frontend/src/pages/InRoom.tsx | 92 ++++++++++++-- frontend/src/pages/Index.tsx | 12 +- frontend/src/pages/JoinRoom.tsx | 33 ++++- 4 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/PreJoinPreview.tsx 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 c6fad6a..83325ad 100644 --- a/frontend/src/pages/InRoom.tsx +++ b/frontend/src/pages/InRoom.tsx @@ -1,19 +1,76 @@ 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"; 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 [localStream, setLocalStream] = useState(null); + // 🔹 STEP 1: Initialize camera and mic on join useEffect(() => { document.title = `${roomName} | PeerCall`; - // TODO: Setup WebRTC connection here in next step + + 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; + }); + setMicOn((prev) => !prev); + }; + + // 🔹 STEP 3: Toggle video + const handleToggleVideo = () => { + if (!localStream) return; + localStream.getVideoTracks().forEach((track) => { + track.enabled = !track.enabled; + }); + setVideoOn((prev) => !prev); + }; + + // 🔹 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 + }; + return (
{/* Header */} @@ -29,21 +86,26 @@ const InRoom: React.FC<{ roomName: string }> = ({ roomName }) => {
{/* Video Grid */}
+ {/* Local Video */}
+ + {/* Remote Video */}
)} @@ -79,26 +145,32 @@ const InRoom: React.FC<{ roomName: string }> = ({ roomName }) => { {/* Controls */}
- 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

-
+ Date: Fri, 7 Nov 2025 23:38:39 +0530 Subject: [PATCH 2/2] feat: updated preview --- frontend/src/pages/InRoom.tsx | 48 ++--------------------------------- 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/frontend/src/pages/InRoom.tsx b/frontend/src/pages/InRoom.tsx index 6c37655..83325ad 100644 --- a/frontend/src/pages/InRoom.tsx +++ b/frontend/src/pages/InRoom.tsx @@ -9,13 +9,6 @@ import { 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); @@ -145,10 +138,7 @@ const InRoom: React.FC<{ roomName: string }> = ({ roomName }) => { Send
-
- ))} -
-
+ )}
@@ -186,41 +176,7 @@ const InRoom: React.FC<{ roomName: string }> = ({ roomName }) => {
- - {/* Controls */} - -
- - ); + ); }; export default InRoom;