diff --git a/frontend/src/pages/InRoom.tsx b/frontend/src/pages/InRoom.tsx index b33ceb7..6ba0f59 100644 --- a/frontend/src/pages/InRoom.tsx +++ b/frontend/src/pages/InRoom.tsx @@ -1,12 +1,12 @@ import React, { useState, useEffect, useRef } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { io, Socket } from "socket.io-client"; -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"; -import { useConnectionQuality } from "../hooks/useConnectionQuality"; -import ConnectionQualityIndicator from "../components/ConnectionQualityIndicator"; -import { API_ENDPOINTS } from "../lib/apiConfig"; +import { useConnectionQuality } from "../hooks/useConnectionQuality.js"; +import ConnectionQualityIndicator from "../components/ConnectionQualityIndicator.js"; +import { API_ENDPOINTS } from "../lib/apiConfig.js"; const keyMap = { TOGGLE_MIC: "ctrl+m", @@ -18,10 +18,11 @@ interface ChatMessage { text: string; time?: string; } + const InRoom: React.FC = () => { const { roomName } = useParams<{ roomName: string }>(); const navigate = useNavigate(); - + const [micOn, setMicOn] = useState(true); const [videoOn, setVideoOn] = useState(true); const [showChat, setShowChat] = useState(false); @@ -30,6 +31,9 @@ const InRoom: React.FC = () => { const [userName, setUserName] = useState("User"); const [participantCount, setParticipantCount] = useState(1); + const [isSharing, setIsSharing] = useState(false); + const [screenStream, setScreenStream] = useState(null); + const socketRef = useRef(null); const localVideoRef = useRef(null); const remoteVideoRef = useRef(null); @@ -46,27 +50,75 @@ const InRoom: React.FC = () => { useEffect(() => { const token = localStorage.getItem("token"); if (token) { - // Try to get user info from token or make API call - // For now, use a default or extract from token const storedName = localStorage.getItem("userName"); if (storedName) { setUserName(storedName); + } else { + // simple fallback: derive from token if you have a scheme + setUserName("User"); } } }, []); - // Initialize Socket.io connection + // --- Helper: create RTCPeerConnection and attach handlers --- + const createPeerConnection = (isInitiator = false) => { + if (peerConnectionRef.current) return peerConnectionRef.current; + + const pc = new RTCPeerConnection({ + iceServers: [ + { urls: "stun:stun.l.google.com:19302" }, + // add TURN if you have one for production + ], + }); + + // Send ICE candidates to remote via signaling + pc.onicecandidate = (event) => { + if (event.candidate && socketRef.current && roomName) { + socketRef.current.emit("rtc-ice-candidate", { + roomId: roomName, + candidate: event.candidate, + }); + } + }; + + // When remote track arrives, attach to remote video element + pc.ontrack = (event) => { + // For typical one-stream-per-peer, event.streams[0] is the remote stream + const [stream] = event.streams; + if (remoteVideoRef.current && stream) { + remoteVideoRef.current.srcObject = stream; + } + }; + + // Add local audio/video tracks if available + const localStream = mediaStreamRef.current; + if (localStream) { + localStream.getTracks().forEach((track) => { + try { + pc.addTrack(track, localStream); + } catch (err) { + console.warn("addTrack failed:", err); + } + }); + } + + peerConnectionRef.current = pc; + return pc; + }; + + // --- Signaling handlers --- useEffect(() => { if (!roomName) return; - document.title = `${roomName} | PeerCall`; - const socket = io(API_ENDPOINTS.SOCKET); socketRef.current = socket; - socket.emit("join-room", roomName, userName); - console.log("🟢 Joined room:", roomName, "as", userName); + socket.on("connect", () => { + // join after connect + socket.emit("join-room", roomName, userName); + }); + // Chat related socket.on("chat-history", (history: any[]) => { setMessages( history.map((m) => ({ @@ -89,33 +141,95 @@ const InRoom: React.FC = () => { }); socket.on("user-joined", ({ userName: joinedUser, roomId }: { userName: string; roomId?: string }) => { - setMessages((prev) => [ - ...prev, - { user: "System", text: `${joinedUser} joined the room` }, - ]); + setMessages((prev) => [...prev, { user: "System", text: `${joinedUser} joined the room` }]); setParticipantCount((prev) => prev + 1); + + // If someone joined and we already have a local stream, create a peer connection and be the caller (initiator) + // This helps existing participant initiate connection to the new joiner. + // For simplicity we broadcast offers to the room; new participant will answer them. + // Only create offer if we have local media ready. + if (mediaStreamRef.current) { + (async () => { + const pc = createPeerConnection(true); + try { + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + socket.emit("rtc-offer", { roomId: roomName, sdp: offer }); + } catch (err) { + console.error("Failed to create/send offer:", err); + } + })(); + } }); socket.on("user-left", ({ userName: leftUser }: { userName: string }) => { - setMessages((prev) => [ - ...prev, - { user: "System", text: `${leftUser} left the room` }, - ]); + setMessages((prev) => [...prev, { user: "System", text: `${leftUser} left the room` }]); setParticipantCount((prev) => Math.max(1, prev - 1)); + // If peer left, close pc + if (peerConnectionRef.current) { + try { + peerConnectionRef.current.close(); + } catch (e) { } + peerConnectionRef.current = null; + if (remoteVideoRef.current) remoteVideoRef.current.srcObject = null; + } }); socket.on("update-members", (members: any[]) => { setParticipantCount(members.length || 1); }); + // RTC Offer: other participant sent an offer -> set remote desc and create answer + socket.on("rtc-offer", async (payload: { roomId?: string; sdp: RTCSessionDescriptionInit }) => { + try { + // If we don't have a pc yet, create one + const pc = createPeerConnection(false); + await pc.setRemoteDescription(new RTCSessionDescription(payload.sdp)); + // create answer + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + socket.emit("rtc-answer", { roomId: roomName, sdp: answer }); + } catch (err) { + console.error("Error handling rtc-offer:", err); + } + }); + + // RTC Answer: remote answered our offer -> set remote description + socket.on("rtc-answer", async (payload: { roomId?: string; sdp: RTCSessionDescriptionInit }) => { + try { + const pc = peerConnectionRef.current; + if (!pc) { + console.warn("Received answer but no peerConnection exists"); + return; + } + await pc.setRemoteDescription(new RTCSessionDescription(payload.sdp)); + } catch (err) { + console.error("Error handling rtc-answer:", err); + } + }); + + // ICE candidate from remote -> add to pc + socket.on("rtc-ice-candidate", async (payload: { roomId?: string; candidate: RTCIceCandidateInit }) => { + try { + const pc = peerConnectionRef.current; + if (!pc || !payload?.candidate) return; + await pc.addIceCandidate(new RTCIceCandidate(payload.candidate)); + } catch (err) { + console.error("Error adding remote ICE candidate:", err); + } + }); + return () => { - socket.emit("leave-room", { - roomId: roomName, - userId: localStorage.getItem("token") || "", - userName, - }); - socket.disconnect(); + if (socketRef.current) { + socketRef.current.emit("leave-room", { + roomId: roomName, + userId: localStorage.getItem("token") || "", + userName, + }); + socketRef.current.disconnect(); + } }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [roomName, userName]); // Initialize media stream @@ -127,9 +241,7 @@ const InRoom: React.FC = () => { audio: true, }); mediaStreamRef.current = stream; - if (localVideoRef.current) { - localVideoRef.current.srcObject = stream; - } + if (localVideoRef.current) localVideoRef.current.srcObject = stream; setMicOn(stream.getAudioTracks().some((t) => t.enabled)); setVideoOn(stream.getVideoTracks().some((t) => t.enabled)); } catch (err) { @@ -147,6 +259,26 @@ const InRoom: React.FC = () => { }; }, []); + // When local media becomes available and a peer is present, ensure tracks are added + useEffect(() => { + // If we have a pc and a media stream, make sure tracks are added + const pc = peerConnectionRef.current; + const stream = mediaStreamRef.current; + if (pc && stream) { + // Avoid double adding by checking pc.getSenders() + const existingKinds = pc.getSenders().map((s) => s.track?.kind).filter(Boolean); + stream.getTracks().forEach((track) => { + if (!existingKinds.includes(track.kind)) { + try { + pc.addTrack(track, stream); + } catch (err) { + console.warn("addTrack (later) failed:", err); + } + } + }); + } + }, [mediaStreamRef.current]); // note: this effect will run once after media sets + // Auto-scroll chat to bottom useEffect(() => { if (showChat) { @@ -191,7 +323,18 @@ const InRoom: React.FC = () => { }; const handleEndCall = () => { + // stop local stream mediaStreamRef.current?.getTracks().forEach((track) => track.stop()); + // stop screen stream (if any) + screenStream?.getTracks().forEach((t) => t.stop()); + // close pc + if (peerConnectionRef.current) { + try { + peerConnectionRef.current.close(); + } catch (e) { } + peerConnectionRef.current = null; + } + // disconnect socket socketRef.current?.disconnect(); navigate("/"); }; @@ -210,6 +353,98 @@ const InRoom: React.FC = () => { setChatInput(""); }; + // ---------------- Screen share logic ---------------- + const replaceSendersWithTrack = (track: MediaStreamTrack | null) => { + const pc = peerConnectionRef.current; + if (!pc) return; + pc.getSenders().forEach((sender) => { + if (sender.track && sender.track.kind === "video") { + sender.replaceTrack(track).catch((err) => console.warn("replaceTrack error:", err)); + } + }); + }; + + const startScreenShare = async () => { + if (!navigator.mediaDevices || !(navigator.mediaDevices as any).getDisplayMedia) { + alert("Screen sharing is not supported in this browser."); + return; + } + try { + const stream = await (navigator.mediaDevices as any).getDisplayMedia({ + video: true, + audio: false, // optional: set true to capture system audio when supported + }); + + const screenTrack = stream.getVideoTracks()[0]; + if (!screenTrack) throw new Error("No screen track"); + + // When the user stops sharing using browser UI: + screenTrack.onended = () => { + stopScreenShare(true); + }; + + // Replace outgoing video senders with the screen track + replaceSendersWithTrack(screenTrack); + + setScreenStream(stream); + setIsSharing(true); + + // Notify other participants so UI can pin the sharer + socketRef.current?.emit("user-screen-sharing", { userName, sharing: true }); + + // Keep a local preview of the screen: show on local video element (optional) + if (localVideoRef.current) localVideoRef.current.srcObject = stream; + } catch (err) { + console.error("startScreenShare error:", err); + } + }; + + const stopScreenShare = (fromTrackEnd = false) => { + // Stop any screen tracks + screenStream?.getTracks().forEach((t) => t.stop()); + + // Restore camera track + const cameraTrack = mediaStreamRef.current?.getVideoTracks()[0] || null; + replaceSendersWithTrack(cameraTrack); + + // Restore local preview to camera stream + if (localVideoRef.current) localVideoRef.current.srcObject = mediaStreamRef.current; + + setIsSharing(false); + setScreenStream(null); + + // Notify peers + socketRef.current?.emit("user-screen-sharing", { userName, sharing: false }); + // If fromTrackEnd true, the onended handler already called this path; emitting twice is harmless + }; + + const toggleScreenShare = () => { + if (isSharing) stopScreenShare(); + else startScreenShare(); + }; + + // Listen for remote sharing UI hints (server should forward this event) + useEffect(() => { + const socket = socketRef.current; + if (!socket) return; + const onSharing = ({ userName: sharerName, sharing }: { userName: string; sharing: boolean }) => { + if (sharing) { + setMessages((prev) => [...prev, { user: "System", text: `${sharerName} is sharing their screen` }]); + // Optionally: if sharer is remote, you could enlarge the remote video tile + // Implementation depends on your layout state (not provided here) + } else { + setMessages((prev) => [...prev, { user: "System", text: `${sharerName} stopped sharing` }]); + } + }; + socket.on("user-screen-sharing", onSharing); + return () => { + socket.off("user-screen-sharing", onSharing); + }; + }, []); + + // ======================================================= + // UI + // ======================================================= if (!roomName) { return (
@@ -234,7 +469,9 @@ const InRoom: React.FC = () => { />
- {participantCount} Participant{participantCount !== 1 ? "s" : ""} + + {participantCount} Participant{participantCount !== 1 ? "s" : ""} +
@@ -245,61 +482,33 @@ const InRoom: React.FC = () => {
{/* Local Video */}
-