Files
videopeersjs/client/src/pages/room/[slug].jsx
ale 34750bcdb1 room fix
Signed-off-by: ale <ale@manalejandro.com>
2025-09-16 02:21:07 +02:00

449 líneas
18 KiB
JavaScript

import { useSocket } from '@/context/SocketProvider';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react'
import peer from '@/service/peer';
import { motion } from 'framer-motion';
import { Video, Phone, Users, Send, ArrowLeft } from 'lucide-react';
import VideoPlayer from '@/components/VideoPlayer';
import CallHandleButtons from '@/components/CallHandleButtons';
const RoomPage = () => {
const socket = useSocket();
const router = useRouter();
const { slug } = router.query;
const [remoteSocketId, setRemoteSocketId] = useState(null);
const [myStream, setMyStream] = useState(null);
const [remoteStream, setRemoteStream] = useState(null);
const [isAudioMute, setIsAudioMute] = useState(false);
const [isVideoOnHold, setIsVideoOnHold] = useState(false);
const [callButton, setCallButton] = useState(true);
const [isSendButtonVisible, setIsSendButtonVisible] = useState(true);
const [isConnecting, setIsConnecting] = useState(false);
const [hasJoinedRoom, setHasJoinedRoom] = useState(false);
// Check if user came from lobby (has proper session) or direct navigation
useEffect(() => {
// Check if user has a valid session or email stored (you might want to implement proper session management)
const hasValidSession = localStorage.getItem('userEmail') || document.referrer.includes('/');
if (!hasValidSession && typeof window !== 'undefined') {
// User navigated directly to room page without going through lobby
console.warn('Direct navigation to room detected, redirecting to lobby');
router.push('/');
return;
}
// If user has a session, mark as joined
setHasJoinedRoom(true);
}, [router]);
// Store user email when they join from lobby
useEffect(() => {
socket.on("room:join", (data) => {
if (typeof window !== 'undefined') {
localStorage.setItem('userEmail', data.email);
localStorage.setItem('currentRoom', data.room);
}
});
// Listen for socket errors and provide user feedback
socket.on("error", (error) => {
console.error('Socket error:', error);
alert(`Connection Error: ${error.message || 'Something went wrong. Please try again.'}`);
// Redirect to lobby on error
router.push('/');
});
return () => {
socket.off("room:join");
socket.off("error");
};
}, [socket, router]);
const handleUserJoined = useCallback(({ email, id }) => {
console.log(`User ${email} joined the room!`);
setRemoteSocketId(id);
}, []);
const handleUserLeft = useCallback(({ email }) => {
console.log(`User ${email} left the room`);
setRemoteSocketId(null);
setRemoteStream(null);
}, []);
const handleIncomingCall = useCallback(async ({ from, offer }) => {
setRemoteSocketId(from);
setIsConnecting(true);
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
setMyStream(stream);
const ans = await peer.getAnswer(offer);
socket.emit("call:accepted", { to: from, ans });
} catch (error) {
console.error('Error handling incoming call:', error);
setIsConnecting(false);
}
}, [socket]);
const sendStreams = useCallback(() => {
if (myStream) {
for (const track of myStream.getTracks()) {
peer.peer.addTrack(track, myStream);
}
setIsSendButtonVisible(false);
}
}, [myStream]);
const handleCallAccepted = useCallback(({ from, ans }) => {
peer.setLocalDescription(ans);
console.log("Call Accepted");
setIsConnecting(false);
sendStreams();
}, [sendStreams]);
const handleNegoNeededIncoming = useCallback(async ({ from, offer }) => {
const ans = await peer.getAnswer(offer);
socket.emit("peer:nego:done", { to: from, ans });
}, [socket]);
const handleNegoNeeded = useCallback(async () => {
const offer = await peer.getOffer();
socket.emit("peer:nego:needed", { offer, to: remoteSocketId });
}, [remoteSocketId, socket]);
const handleNegoFinal = useCallback(async ({ ans }) => {
await peer.setLocalDescription(ans);
}, [])
useEffect(() => {
peer.peer.addEventListener('negotiationneeded', handleNegoNeeded);
return () => {
peer.peer.removeEventListener('negotiationneeded', handleNegoNeeded);
}
}, [handleNegoNeeded]);
useEffect(() => {
peer.peer.addEventListener('track', async ev => {
const remoteStream = ev.streams;
console.log("GOT TRACKS!");
setRemoteStream(remoteStream[0]);
setIsConnecting(false);
})
}, [])
useEffect(() => {
socket.on("user:joined", handleUserJoined);
socket.on("user:left", handleUserLeft);
socket.on("incoming:call", handleIncomingCall);
socket.on("call:accepted", handleCallAccepted);
socket.on("peer:nego:needed", handleNegoNeededIncoming);
socket.on("peer:nego:final", handleNegoFinal);
return () => {
socket.off("user:joined", handleUserJoined);
socket.off("user:left", handleUserLeft);
socket.off("incoming:call", handleIncomingCall);
socket.off("call:accepted", handleCallAccepted);
socket.off("peer:nego:needed", handleNegoNeededIncoming);
socket.off("peer:nego:final", handleNegoFinal);
};
}, [
socket,
handleUserJoined,
handleUserLeft,
handleIncomingCall,
handleCallAccepted,
handleNegoNeededIncoming,
handleNegoFinal
]);
useEffect(() => {
socket.on("call:end", ({ from }) => {
if (from === remoteSocketId) {
peer.peer.close();
if (myStream) {
myStream.getTracks().forEach(track => track.stop());
setMyStream(null);
}
setRemoteStream(null);
setRemoteSocketId(null);
setCallButton(true);
setIsSendButtonVisible(true);
setIsConnecting(false);
}
});
return () => {
socket.off("call:end");
}
}, [remoteSocketId, myStream, socket]);
useEffect(() => {
socket.on("call:initiated", ({ from }) => {
if (from === remoteSocketId) {
setCallButton(false);
}
});
return () => {
socket.off("call:initiated");
}
}, [socket, remoteSocketId]);
const handleCallUser = useCallback(async () => {
setIsConnecting(true);
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
if (isAudioMute) {
const audioTracks = stream.getAudioTracks();
audioTracks.forEach(track => track.enabled = false);
}
if (isVideoOnHold) {
const videoTracks = stream.getVideoTracks();
videoTracks.forEach(track => track.enabled = false);
}
const offer = await peer.getOffer();
socket.emit("user:call", { to: remoteSocketId, offer })
setMyStream(stream);
setCallButton(false);
socket.emit("call:initiated", { to: remoteSocketId });
} catch (error) {
console.error('Error starting call:', error);
setIsConnecting(false);
}
}, [remoteSocketId, socket, isAudioMute, isVideoOnHold]);
const handleToggleAudio = () => {
if (myStream) {
const audioTracks = myStream.getAudioTracks();
audioTracks.forEach(track => track.enabled = !track.enabled);
setIsAudioMute(!isAudioMute);
}
};
const handleToggleVideo = () => {
if (myStream) {
const videoTracks = myStream.getVideoTracks();
videoTracks.forEach(track => track.enabled = !track.enabled);
setIsVideoOnHold(!isVideoOnHold);
}
}
const handleEndCall = useCallback(() => {
peer.peer.close();
if (myStream) {
myStream.getTracks().forEach(track => track.stop());
setMyStream(null);
}
setRemoteStream(null);
setCallButton(true);
setIsSendButtonVisible(true);
setIsConnecting(false);
if (remoteSocketId) {
socket.emit("call:end", { to: remoteSocketId });
}
setRemoteSocketId(null);
}, [myStream, remoteSocketId, socket]);
const handleGoBack = () => {
handleEndCall();
// Clear session data when leaving room
if (typeof window !== 'undefined') {
localStorage.removeItem('userEmail');
localStorage.removeItem('currentRoom');
}
router.push('/');
};
return (
<>
{!hasJoinedRoom ? (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900 flex items-center justify-center">
<div className="text-center text-white">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent mx-auto mb-4"></div>
<p>Verifying access...</p>
</div>
</div>
) : (
<div className='min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900 relative overflow-hidden'>
<title>Room {slug} - VideoPeersJS</title>
{/* Background decorative elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 -left-4 w-72 h-72 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-pulse"></div>
<div className="absolute bottom-1/4 -right-4 w-72 h-72 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-pulse delay-1000"></div>
</div>
{/* Header */}
<motion.header
className="absolute top-0 left-0 right-0 z-20 p-6"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<motion.button
onClick={handleGoBack}
className="flex items-center space-x-2 px-4 py-2 bg-white/10 backdrop-blur-sm rounded-xl text-white hover:bg-white/20 transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<ArrowLeft className="h-4 w-4" />
<span>Back</span>
</motion.button>
<div className="flex items-center space-x-2">
<Video className="h-6 w-6 text-indigo-400" />
<h1 className="text-xl font-bold text-white">
{process.env.NEXT_PUBLIC_APP_NAME || 'VideoPeersJS'}
</h1>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2 px-4 py-2 bg-white/10 backdrop-blur-sm rounded-xl text-white">
<Users className="h-4 w-4" />
<span className="text-sm">Room {slug}</span>
</div>
</div>
</div>
</motion.header>
{/* Connection Status */}
<motion.div
className="absolute top-24 left-1/2 transform -translate-x-1/2 z-20"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<div className={`px-6 py-3 rounded-full backdrop-blur-sm text-white text-center ${
remoteSocketId ? 'bg-green-500/20 border border-green-400/30' : 'bg-orange-500/20 border border-orange-400/30'
}`}>
<p className="text-sm font-medium">
{isConnecting ? (
<span className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
<span>Connecting...</span>
</span>
) : remoteSocketId ? (
"🟢 Connected with remote user"
) : (
"🟡 Waiting for someone to join..."
)}
</p>
</div>
</motion.div>
{/* Video Container */}
<div className="relative h-screen flex items-center justify-center p-6 pt-32">
{/* Remote Stream (Main) */}
{remoteStream ? (
<VideoPlayer
stream={remoteStream}
name={"Remote Stream"}
isAudioMute={false}
/>
) : (
<motion.div
className="w-full max-w-4xl aspect-video bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl border-2 border-white/10 flex items-center justify-center"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6 }}
>
<div className="text-center text-white/60">
<Users className="h-24 w-24 mx-auto mb-4 opacity-40" />
<p className="text-xl font-medium mb-2">Waiting for remote video...</p>
<p className="text-sm">Share this room ID with someone to start a call</p>
</div>
</motion.div>
)}
{/* My Stream (Picture-in-Picture) */}
{myStream && (
<VideoPlayer
stream={myStream}
name={"My Stream"}
isAudioMute={isAudioMute}
/>
)}
</div>
{/* Action Buttons */}
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-30">
{remoteStream && isSendButtonVisible && (
<motion.button
onClick={sendStreams}
className="mb-4 flex items-center space-x-2 px-6 py-3 bg-green-500 hover:bg-green-600 text-white rounded-xl font-semibold transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Send className="h-5 w-5" />
<span>Send Stream</span>
</motion.button>
)}
{remoteSocketId && callButton && !remoteStream && (
<motion.button
onClick={handleCallUser}
disabled={isConnecting}
className="flex items-center space-x-2 px-8 py-4 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white rounded-2xl font-semibold text-lg shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
whileHover={{ scale: isConnecting ? 1 : 1.05 }}
whileTap={{ scale: isConnecting ? 1 : 0.95 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{isConnecting ? (
<>
<div className="animate-spin rounded-full h-6 w-6 border-2 border-white border-t-transparent"></div>
<span>Connecting...</span>
</>
) : (
<>
<Phone className="h-6 w-6" />
<span>Start Call</span>
</>
)}
</motion.button>
)}
</div>
{/* Call Control Buttons */}
{myStream && remoteStream && !isSendButtonVisible && (
<CallHandleButtons
isAudioMute={isAudioMute}
isVideoOnHold={isVideoOnHold}
onToggleAudio={handleToggleAudio}
onToggleVideo={handleToggleVideo}
onEndCall={handleEndCall}
/>
)}
</div>
)}
</>
)
}
export default RoomPage;