From 5b615873dae668e0c7979589b245cdb993b0e54d Mon Sep 17 00:00:00 2001 From: tanish35 Date: Thu, 31 Oct 2024 10:38:54 +0530 Subject: [PATCH] Added video calling support --- backend/prisma/schema.prisma | 8 ++ backend/src/controllers/videoController.ts | 73 ++++++++++ backend/src/index.ts | 4 +- backend/src/routes/videoRoutes.ts | 11 ++ frontend/package.json | 1 + frontend/src/components/StartVideoCall.jsx | 137 +++++++++++++++++++ frontend/src/components/VideoCall.jsx | 95 +++++++++++++ frontend/src/components/routes/mainroute.jsx | 10 ++ 8 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 backend/src/controllers/videoController.ts create mode 100644 backend/src/routes/videoRoutes.ts create mode 100644 frontend/src/components/StartVideoCall.jsx create mode 100644 frontend/src/components/VideoCall.jsx diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 163d067..42074ae 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -24,6 +24,7 @@ model User { sentMessages Message[] @relation("SentMessages") Like Like[] Otp Otp[] + Video Video[] } model Otp { @@ -131,3 +132,10 @@ model Like { User User @relation(fields: [user_id], references: [user_id]) Post Post @relation(fields: [post_id], references: [post_id]) } + +model Video{ + video_id String @id @default(cuid()) + url String + user_id String + User User @relation(fields: [user_id], references: [user_id]) +} diff --git a/backend/src/controllers/videoController.ts b/backend/src/controllers/videoController.ts new file mode 100644 index 0000000..7e3f0e4 --- /dev/null +++ b/backend/src/controllers/videoController.ts @@ -0,0 +1,73 @@ +import asyncHandler from "express-async-handler"; +import { Request, Response } from "express"; +import axios from "axios"; +import prisma from "../lib/prisma"; + +export const createRoom = asyncHandler(async (req: Request, res: Response) => { + //@ts-ignore + const username = req.user.username; + const video = await prisma.video.findUnique({ + where: { + video_id: username, + }, + }); + if (video) { + res.status(205).json({ message: "Room already exists" }); + return; + } + const response = await axios.post( + "https://api.daily.co/v1/rooms", + { + properties: { enable_chat: true, enable_screenshare: true }, + name: username, + }, + { headers: { Authorization: `Bearer ${process.env.DAILY_API_KEY}` } } + ); + if (!response.data) { + res.status(400); + throw new Error("Invalid data"); + } + const url = response.data.url; + const video_id = response.data.name; + //@ts-ignore + const user_id = req.user.user_id; + await prisma.video.create({ + data: { + video_id, + url, + user_id, + }, + }); + res.status(201).json(response.data); +}); + +export const deleteRoom = asyncHandler(async (req: Request, res: Response) => { + const { video_id } = req.body; + if (!video_id) { + res.status(400); + throw new Error("Invalid data"); + } + const video = await prisma.video.findUnique({ + where: { + video_id, + }, + select: { + user_id: true, + }, + }); + //@ts-ignore + const user_id = req.user.user_id; + if (video?.user_id !== user_id) { + res.status(401).json({ message: "Unauthorized" }); + return; + } + await axios.delete(`https://api.daily.co/v1/rooms/${video_id}`, { + headers: { Authorization: `Bearer ${process.env.DAILY_API_KEY}` }, + }); + await prisma.video.delete({ + where: { + video_id, + }, + }); + res.status(200).json({ message: "Room deleted" }); +}); diff --git a/backend/src/index.ts b/backend/src/index.ts index c7ddbd7..fce5045 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -12,7 +12,8 @@ import reviewRoutes from "./routes/reviewRoutes"; import ratingRoutes from "./routes/ratingRoute"; import postsRoutes from "./routes/postsRoutes"; import roomRouter from "./routes/roomRoutes"; -import Otprouter from "./routes/otpRoute"; +import Otprouter from "./routes/otpRoute"; +import videoRouter from "./routes/videoRoutes"; // import { getCommunities } from "./controllers/postController"; @@ -41,6 +42,7 @@ app.use("/api/chat", chatRoutes); // Use the chat routes app.use("/api/post", postsRoutes); app.use("/api/room", roomRouter); app.use("/api/otp", Otprouter); +app.use("/api/video", videoRouter); // app.get("/api/post/communities", getCommunities); app.get("/api/logout", (req: Request, res: Response) => { res.clearCookie("Authorization").json({ message: "Logged out successfully" }); diff --git a/backend/src/routes/videoRoutes.ts b/backend/src/routes/videoRoutes.ts new file mode 100644 index 0000000..343d848 --- /dev/null +++ b/backend/src/routes/videoRoutes.ts @@ -0,0 +1,11 @@ +import express from "express"; +import checkAuth from "../middleware/checkAuth"; +import { createRoom, deleteRoom } from "../controllers/videoController"; +import rateLimiter from "../middleware/rateLimit"; + +const router = express.Router(); + +router.post("/createroom", checkAuth, rateLimiter, createRoom); +router.delete("/deleteroom", checkAuth, rateLimiter, deleteRoom); + +export default router; diff --git a/frontend/package.json b/frontend/package.json index ca5d8b0..315bf99 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@chakra-ui/react": "^2.8.2", + "@daily-co/daily-js": "^0.72.1", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", "axios": "^1.7.2", diff --git a/frontend/src/components/StartVideoCall.jsx b/frontend/src/components/StartVideoCall.jsx new file mode 100644 index 0000000..31c26e0 --- /dev/null +++ b/frontend/src/components/StartVideoCall.jsx @@ -0,0 +1,137 @@ +import { useState, useRef, useEffect } from "react"; +import { + Box, + Button, + Center, + Heading, + VStack, + useToast, + Flex, + Input, +} from "@chakra-ui/react"; +import { gsap } from "gsap"; +import { useNavigate } from "react-router-dom"; +import VideoCall from "./VideoCall"; +import { useUser } from "../hook/useUser"; +import axios from "axios"; +import "../styles/loader.css"; +// import { useNavigate } from "react-router-dom"; + +const StartVideoCall = () => { + const toast = useToast(); + const navigate = useNavigate(); + const { userDetails, loadingUser } = useUser(); + + const [isLoading, setIsLoading] = useState(loadingUser); + const [roomUrl, setRoomUrl] = useState(""); // URL for the created room + const [joinRoomUrl, setJoinRoomUrl] = useState(""); // URL for joining a room + const buttonRef = useRef(); + + useEffect(() => { + if (loadingUser) { + setIsLoading(true); + } else { + setIsLoading(false); + if (!userDetails) { + toast({ + title: "Please sign in first", + status: "error", + duration: 5000, + isClosable: true, + }); + navigate("/login"); + } + } + }, [loadingUser, userDetails]); + + const createRoom = async () => { + try { + const response = await axios.post("/api/video/createroom"); + if (response.status == 205) { + navigate(`/video/${userDetails.username}`); + return; + } + setRoomUrl(response.data.url); + setJoinRoomUrl(""); + navigate(`/video/${response.data.url.split("/").pop()}`); + } catch (error) { + console.error("Failed to create room", error); + toast({ + title: "Error creating room", + status: "error", + duration: 5000, + isClosable: true, + }); + } + }; + + const handleUrlChange = (e) => { + setJoinRoomUrl(e.target.value); + }; + + const handleJoinRoom = () => { + if (joinRoomUrl) { + setRoomUrl(joinRoomUrl); + setJoinRoomUrl(""); + navigate(`/video/${joinRoomUrl.split("/").pop()}`); + } else { + toast({ + title: "Please enter a valid room URL", + status: "warning", + duration: 5000, + isClosable: true, + }); + } + }; + + useEffect(() => { + if (!isLoading) { + gsap.from(buttonRef.current, { + y: -50, + opacity: 0, + duration: 0.5, + ease: "bounce", + }); + } + }, [isLoading]); + + if (isLoading) { + return ( + +
+
+ ); + } + + return ( +
+ + + {roomUrl ? "Join the Video Call" : "Ready to Start a Video Call?"} + + + + + +
+ ); +}; + +export default StartVideoCall; diff --git a/frontend/src/components/VideoCall.jsx b/frontend/src/components/VideoCall.jsx new file mode 100644 index 0000000..fbe5332 --- /dev/null +++ b/frontend/src/components/VideoCall.jsx @@ -0,0 +1,95 @@ +import DailyIframe from "@daily-co/daily-js"; +import { useEffect, useRef, useState } from "react"; +import { Box, useToast, Flex } from "@chakra-ui/react"; +import { useParams, useNavigate } from "react-router-dom"; +import { useUser } from "../hook/useUser"; +import axios from "axios"; +import "../styles/loader.css"; + +const VideoCall = () => { + const videoRef = useRef(); + const callFrame = useRef(); + const { id } = useParams(); + const navigate = useNavigate(); + const roomUrl = "https://campusify.daily.co/" + id; + const { userDetails, loadingUser } = useUser(); + const toast = useToast(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (loadingUser) { + setIsLoading(true); + } else { + setIsLoading(false); + if (!userDetails) { + toast({ + title: "Please sign in first", + status: "error", + duration: 5000, + isClosable: true, + }); + navigate("/login"); + } + } + }, [loadingUser, userDetails, navigate, toast]); + + const username = userDetails?.username; + + useEffect(() => { + if (!isLoading && username) { + callFrame.current = DailyIframe.createFrame(videoRef.current, { + showLeaveButton: true, + iframeStyle: { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + borderRadius: "0", + }, + }); + const handleLeave = async () => { + const videoId = roomUrl.split("/").pop(); + try { + const response = await axios.delete("/api/video/deleteroom", { + data: { video_id: videoId }, + }); + console.log(response.data.message); + } catch (error) { + console.error("Failed to delete room:", error); + } + }; + callFrame.current.on("left-meeting", handleLeave); + + // console.log(username, roomUrl); + callFrame.current.join({ url: roomUrl, userName: username }); + + return () => { + callFrame.current.leave(); + callFrame.current.destroy(); + }; + } + }, [roomUrl, username, isLoading]); + + if (isLoading) { + return ( + +
+
+ ); + } + + return ( + + ); +}; + +export default VideoCall; diff --git a/frontend/src/components/routes/mainroute.jsx b/frontend/src/components/routes/mainroute.jsx index 17f0765..52384d8 100644 --- a/frontend/src/components/routes/mainroute.jsx +++ b/frontend/src/components/routes/mainroute.jsx @@ -6,6 +6,8 @@ import { Outlet } from "react-router-dom"; import { chatRoomApi } from "../contexts/chatRoomApi"; import { useState } from "react"; import Posts from "../../components/Posts"; +import StartVideoCall from "../StartVideoCall"; +import VideoCall from "../VideoCall"; import Register from "../Register"; import SinglePost from "../../components/SinglePost"; @@ -104,6 +106,14 @@ const Mainrouter = createBrowserRouter([ path: "/edit", element: , }, + { + path: "/call", + element: , + }, + { + path: "/video/:id", + element: , + }, { path: "room", element: ,