diff --git a/client/app/doctor/video-call/[sectionId]/Join-page.tsx b/client/app/doctor/video-call/[sectionId]/Join-page.tsx new file mode 100644 index 00000000..4ce52724 --- /dev/null +++ b/client/app/doctor/video-call/[sectionId]/Join-page.tsx @@ -0,0 +1,41 @@ +import { Button } from "@/components/ui/button" +import { IVideoSection } from "@/types/entities"; +import { Video } from "lucide-react" + +interface JoinPageProps { + onJoin: () => void; + section:IVideoSection +} + +export default function JoinPage({ onJoin , section}: JoinPageProps) { + // useEffect(() => { + // if (section) { + // const checkMeetingTime = () => { + // const now = new Date() + // const meetingTime = new Date(section.startTime!) + // const timeDiff = meetingTime.getTime() - now.getTime() + // const minutesDiff = Math.floor(timeDiff / (1000 * 60)) + // setCanStartMeeting(minutesDiff <= 10 && minutesDiff >= 0) + // } + + // checkMeetingTime() + // const timer = setInterval(checkMeetingTime, 60000) + + // return () => clearInterval(timer) + // } + // }, [section]) + + + return ( +
+
+

Welcome to Your Video Call

+

Click the button below to join the room

+
+ +
+ ) +} \ No newline at end of file diff --git a/client/app/doctor/video-call/[sectionId]/page.tsx b/client/app/doctor/video-call/[sectionId]/page.tsx index 5d9e3d87..4af1493f 100644 --- a/client/app/doctor/video-call/[sectionId]/page.tsx +++ b/client/app/doctor/video-call/[sectionId]/page.tsx @@ -1,56 +1,62 @@ 'use client' -import VideoChat from '@/components/page-components/video/VideoChat'; -import { useGetSectionByIdDoctor } from '@/lib/hooks/video/useDoctor'; -import { useParams } from 'next/navigation'; -import { useEffect, useState } from 'react'; - -const VideoCallPage = () => { - const { sectionId } = useParams(); - const [isCalling, setIsCalling] = useState(false); - const [localStream, setLocalStream] = useState(null); - const [remoteStream, setRemoteStream] = useState(null); - const { data, isLoading } = useGetSectionByIdDoctor(sectionId as string); - const section = data?.section - useEffect(() => { - const timer = setTimeout(() => { - setRemoteStream(new MediaStream()); - }, 2000); - - return () => clearTimeout(timer); - }, [sectionId]); - - const handleStartCall = async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); - setLocalStream(stream); - setIsCalling(true); - } catch (error) { - console.error('Error accessing media devices:', error); - } - }; - - const handleEndCall = () => { - if (localStream) { - localStream.getTracks().forEach(track => track.stop()); - } - setLocalStream(null); - setRemoteStream(null); - setIsCalling(false); - }; - - if (isLoading) return
Loading...
; - - return ( - - ); -} - -export default VideoCallPage; + +import { useEffect, useState } from 'react' +import { useParams } from 'next/navigation' +import { useGetSectionByIdDoctor } from '@/lib/hooks/video/useDoctor' +import JoinPage from './Join-page' +import VideoChat from '@/components/page-components/video/VideoChat' + +export default function VideoCallPage() { + const { sectionId } = useParams() + const [hasJoined, setHasJoined] = useState(false) + const [localStream, setLocalStream] = useState(null) + const [remoteStream, setRemoteStream] = useState(null) + const { data, isLoading } = useGetSectionByIdDoctor(sectionId as string) + const section = data?.section + + useEffect(() => { + if (hasJoined) { + const timer = setTimeout(() => { + setRemoteStream(new MediaStream()) + }, 2000) + + return () => clearTimeout(timer) + } + }, [hasJoined, sectionId]) + + const handleJoin = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) + setLocalStream(stream) + setHasJoined(true) + } catch (error) { + console.error('Error accessing media devices:', error) + } + } + + const handleEndCall = () => { + if (localStream) { + localStream.getTracks().forEach(track => track.stop()) + } + setLocalStream(null) + setRemoteStream(null) + setHasJoined(false) + } + + if (isLoading) return
Loading...
+ + if (!hasJoined) { + return + } + + return ( + + ) +} \ No newline at end of file diff --git a/client/components/button/VideoSectionButtonDoctor.tsx b/client/components/button/VideoSectionButtonDoctor.tsx index e70b372b..813219f3 100644 --- a/client/components/button/VideoSectionButtonDoctor.tsx +++ b/client/components/button/VideoSectionButtonDoctor.tsx @@ -40,7 +40,7 @@ const VideoSectionButtonDoctor = forwardRef((props, ref) => { open={isModalOpen} setOpen={setIsModalOpen} sections={upcomingSections!} - link="/doctor/video-section" + link="/doctor/video-call" user="doctor" /> diff --git a/client/components/layout/DoctorLayoutWithSideBar.tsx b/client/components/layout/DoctorLayoutWithSideBar.tsx index 7ef6399c..027022a2 100644 --- a/client/components/layout/DoctorLayoutWithSideBar.tsx +++ b/client/components/layout/DoctorLayoutWithSideBar.tsx @@ -41,6 +41,8 @@ const AdminLayoutWithSideBar = ({ const { setCredentials } = useAuth(); const router = useRouter(); + const isVideoCall = pathname.includes("/video-call/") + const handleLogout = () => { logout( {}, @@ -171,7 +173,7 @@ const AdminLayoutWithSideBar = ({ -
+
diff --git a/client/components/page-components/video/ControlPanel.tsx b/client/components/page-components/video/ControlPanel.tsx deleted file mode 100644 index 36ef39a9..00000000 --- a/client/components/page-components/video/ControlPanel.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { ButtonV2 } from "@/components/button/ButtonV2" -import { Mic, MicOff, Video, VideoOff, PhoneOff } from 'lucide-react' -import { useState } from "react" - -interface ControlPanelProps { - onEndCall: () => void -} - -export default function ControlPanel({ onEndCall }: ControlPanelProps) { - const [isMuted, setIsMuted] = useState(false) - const [isVideoOff, setIsVideoOff] = useState(false) - - const toggleMute = () => { - setIsMuted(!isMuted); - } - - const toggleVideo = () => { - setIsVideoOff(!isVideoOff); - } - - return ( -
-
- - {isMuted ? : } - - - {isVideoOff ? : - - - -
-
- ) -} \ No newline at end of file diff --git a/client/components/page-components/video/UserVideo.tsx b/client/components/page-components/video/UserVideo.tsx deleted file mode 100644 index 6ee03b11..00000000 --- a/client/components/page-components/video/UserVideo.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect, useRef } from "react"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; - -interface UserVideoProps { - stream: MediaStream | null; - isSelf: boolean; - avatarUrl?: string; - fullScreen?: boolean; -} - -export default function UserVideo({ stream, isSelf, avatarUrl, fullScreen = false }: UserVideoProps) { - const videoRef = useRef(null); - - useEffect(() => { - if (videoRef.current && stream) { - videoRef.current.srcObject = stream; - } - }, [stream]); - - const videoClasses = fullScreen - ? "absolute inset-0 w-full h-full object-cover" - : "w-full h-full object-cover rounded-lg"; - - return ( -
- {stream ? ( -
- ); -} diff --git a/client/components/page-components/video/VideoChat.tsx b/client/components/page-components/video/VideoChat.tsx index 92c671b7..1f618fa6 100644 --- a/client/components/page-components/video/VideoChat.tsx +++ b/client/components/page-components/video/VideoChat.tsx @@ -1,27 +1,158 @@ -import UserVideo from './UserVideo'; -import ControlPanel from './ControlPanel'; -import { IVideoSection } from '@/types/entities'; +'use client' + +import { useState, useEffect, useRef } from 'react' +import { Mic, MicOff, Video, VideoOff, PhoneOff } from 'lucide-react' +import { Button } from "@/components/ui/button" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" interface VideoChatProps { - localStream: MediaStream | null; - remoteStream: MediaStream | null; - handleEndCall: () => void; - isCalling: boolean; - isDoctor: boolean; - selfAvatar:string; - remoteAvatar:string; + localStream: MediaStream | null + remoteStream: MediaStream | null + handleEndCall: () => void + isDoctor: boolean + selfAvatar: string + remoteAvatar: string } -export default function VideoChat({ localStream, remoteStream, handleEndCall, isCalling , remoteAvatar, selfAvatar}: VideoChatProps) { +export default function VideoChat({ + localStream, + remoteStream, + handleEndCall, + isDoctor, + selfAvatar, + remoteAvatar +}: VideoChatProps) { + const [isMuted, setIsMuted] = useState(false) + const [isVideoOff, setIsVideoOff] = useState(false) + const [showControls, setShowControls] = useState(true) + const localVideoRef = useRef(null) + const remoteVideoRef = useRef(null) + + useEffect(() => { + if (localVideoRef.current && localStream) { + localVideoRef.current.srcObject = localStream + } + }, [localStream]) + + useEffect(() => { + if (remoteVideoRef.current && remoteStream) { + remoteVideoRef.current.srcObject = remoteStream + } + }, [remoteStream]) + + const toggleMute = () => { + if (localStream) { + localStream.getAudioTracks().forEach(track => track.enabled = !track.enabled) + setIsMuted(!isMuted) + } + } + + const toggleVideo = () => { + if (localStream) { + localStream.getVideoTracks().forEach(track => track.enabled = !track.enabled) + setIsVideoOff(!isVideoOff) + } + } + + const toggleControls = () => { + setShowControls(!showControls) + } + return ( -
-
- -
- -
+
+
+ {remoteStream ? ( +
+
+ {localStream ? ( +
- {isCalling && } + {showControls && ( +
+
+ + + + + + +

{isMuted ? 'Unmute' : 'Mute'}

+
+
+
+ + + + + + +

{isVideoOff ? 'Turn on video' : 'Turn off video'}

+
+
+
+ + + + + + +

End call

+
+
+
+
+
+ )}
- ); -} + ) +} \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 27836918..b5759df9 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -48,6 +48,7 @@ "react-easy-crop": "^5.0.8", "react-hook-form": "^7.52.2", "react-phone-number-input": "^3.4.5", + "socket.io-client": "^4.8.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" @@ -2272,6 +2273,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@stripe/react-stripe-js": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.8.0.tgz", @@ -3682,7 +3689,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -3852,6 +3858,28 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -5994,7 +6022,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -7350,6 +7377,34 @@ "node": ">=8" } }, + "node_modules/socket.io-client": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -8253,6 +8308,35 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/client/package.json b/client/package.json index 3d8e08f5..cf2c3351 100644 --- a/client/package.json +++ b/client/package.json @@ -51,6 +51,7 @@ "react-easy-crop": "^5.0.8", "react-hook-form": "^7.52.2", "react-phone-number-input": "^3.4.5", + "socket.io-client": "^4.8.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8"