diff --git a/frontend/src/components/PermissionErrorModal.tsx b/frontend/src/components/PermissionErrorModal.tsx new file mode 100644 index 0000000..1263acf --- /dev/null +++ b/frontend/src/components/PermissionErrorModal.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { AlertTriangle, RefreshCw, X } from "lucide-react"; + +interface PermissionErrorModalProps { + isOpen: boolean; + message: string; + onRetry: () => void; + onClose: () => void; +} + +const PermissionErrorModal: React.FC = ({ + isOpen, + message, + onRetry, + onClose, +}) => { + return ( + + {isOpen && ( + + {/* Modal Card */} + + {/* Header */} +
+
+ + Device Permission Error +
+ +
+ + {/* Message */} +

{message}

+ + {/* Actions */} +
+ + {/* Retry Button */} + + + {/* Browser Permission Links */} + +
+
+
+ )} +
+ ); +}; + +export default PermissionErrorModal; diff --git a/frontend/src/pages/InRoom.tsx b/frontend/src/pages/InRoom.tsx index 6ba0f59..ce3b25e 100644 --- a/frontend/src/pages/InRoom.tsx +++ b/frontend/src/pages/InRoom.tsx @@ -22,6 +22,9 @@ interface ChatMessage { const InRoom: React.FC = () => { const { roomName } = useParams<{ roomName: string }>(); const navigate = useNavigate(); + const [permissionError, setPermissionError] = useState(null); + const [showPermissionModal, setShowPermissionModal] = useState(false); + const [micOn, setMicOn] = useState(true); const [videoOn, setVideoOn] = useState(true); @@ -233,32 +236,50 @@ const InRoom: React.FC = () => { }, [roomName, userName]); // Initialize media stream - useEffect(() => { - const initMedia = async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - video: true, - audio: true, - }); - mediaStreamRef.current = stream; - if (localVideoRef.current) localVideoRef.current.srcObject = stream; - setMicOn(stream.getAudioTracks().some((t) => t.enabled)); - setVideoOn(stream.getVideoTracks().some((t) => t.enabled)); - } catch (err) { - console.error("Error accessing camera/mic:", err); - alert("Please allow camera and microphone permissions."); - setMicOn(false); - setVideoOn(false); + const initMedia = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: true, + }); + + // Success + setPermissionError(null); + setShowPermissionModal(false); + + // Save stream to your refs/states + mediaStreamRef.current = stream; + setMicOn(true); + setVideoOn(true); + + // Attach tracks, send to peer, whatever your flow is + if (localVideoRef.current) { + localVideoRef.current.srcObject = stream; } - }; - initMedia(); + } catch (err: any) { + console.error("Error accessing camera/mic:", err); - return () => { - mediaStreamRef.current?.getTracks().forEach((track) => track.stop()); - }; + let msg = "Camera/Microphone access was denied. Please allow permissions."; + + if (err.name === "NotAllowedError") { + msg = "You blocked camera/mic access for this site. Please enable it from browser settings."; + } else if (err.name === "NotFoundError") { + msg = "No camera or microphone was found on your device."; + } + + setPermissionError(msg); + setShowPermissionModal(true); + + setMicOn(false); + setVideoOn(false); + } + }; + useEffect(() => { + initMedia(); }, []); + // 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 @@ -452,6 +473,55 @@ const InRoom: React.FC = () => { ); } + const permissionModal = showPermissionModal && ( +
+ +

Permissions Required

+ +

+ {permissionError} +

+ + {/* Instructions when user BLOCKED permissions */} + {permissionError?.includes("blocked") && ( +
+

To enable camera/mic permissions:

+
    +
  • Click the lock icon in the URL bar
  • +
  • Open "Site Settings"
  • +
  • Set Camera and Microphone to "Allow"
  • +
  • Reload the page
  • +
+
+ )} + +
+ + + +
+
+
+ ); + return ( @@ -477,6 +547,8 @@ const InRoom: React.FC = () => { {/* Main Video Area */} + {permissionModal} +
{/* Video Grid */}
diff --git a/frontend/src/pages/JoinRoom.tsx b/frontend/src/pages/JoinRoom.tsx index c8bffbc..8641294 100644 --- a/frontend/src/pages/JoinRoom.tsx +++ b/frontend/src/pages/JoinRoom.tsx @@ -14,15 +14,40 @@ export default function JoinRoom() { const API_BASE = "http://localhost:3000/api/rooms"; // 🔹 Step 1: Handle room join initiation - const handleJoinClick = (e: React.FormEvent) => { + const handleJoinClick = async (e: React.FormEvent) => { e.preventDefault(); + if (!roomName.trim()) return alert("Please enter a room name!"); - // Instead of joining immediately, show preview first - setShowPreview(true); + + // ⬇️ PREVIEW SHOULD ONLY SHOW IF MEDIA PERMISSION IS SUCCESSFUL + try { + const tempStream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: true, + }); + + setStream(tempStream); + setShowPreview(true); + } catch (err: any) { + console.error("Media access denied:", err); + + if (err.name === "NotAllowedError") { + alert( + "You blocked the camera/mic.\n\nPlease enable permissions:\n1. Click lock icon in URL bar\n2. Open Site Settings\n3. Set Camera & Microphone to Allow\n4. Reload the page" + ); + } else { + alert("Unable to access camera/mic: " + err.message); + } + } }; // 🔹 Step 2: After preview confirmation, actually join the backend room const handleConfirmJoin = async (mediaStream: MediaStream) => { + if (!mediaStream) { + alert("Camera or microphone not available!"); + return; + } + setStream(mediaStream); setLoading(true); @@ -39,8 +64,10 @@ export default function JoinRoom() { ); alert("Joined room successfully!"); - // Optional: Stop preview stream before entering actual call - mediaStream.getTracks().forEach(track => track.stop()); + + // 🛑 SAFE STOP (prevents getTracks() crash) + mediaStream.getTracks().forEach((track) => track.stop()); + navigate(`/room/${roomName}`); } catch (err: any) { console.error("Join room error:", err); @@ -52,10 +79,10 @@ export default function JoinRoom() { }; // 🔹 Step 3: Conditional render — preview or join form - if (showPreview) { + if (showPreview && stream) { return (
- +
); } @@ -67,6 +94,7 @@ export default function JoinRoom() {

Join a Room

+
+