Web RTC Video Call

You might also like

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 31

Web rtc video call:

frontend:
import { FormControl } from "@chakra-ui/form-control";

import { Input } from "@chakra-ui/input";


import { Box, Text } from "@chakra-ui/layout";
import { Flex } from "@chakra-ui/layout";
import { Alert, AlertIcon } from "@chakra-ui/react";
import "./styles.css";
import { IconButton, Spinner, useToast } from "@chakra-ui/react";
import { getSender, getSenderFull } from "../config/ChatLogics";
import React, { useState, useEffect, useRef } from 'react';
import axios from "axios";
import { ArrowBackIcon } from "@chakra-ui/icons";
import ProfileModal from "./miscellaneous/ProfileModal";
import { ChatState } from "../Context/ChatProvider";
import UpdateGroupChatModal from "./miscellaneous/UpdateGroupChatModal";
import ScrollableChat from "./ScrollableChat";
import io from "socket.io-client";
import Lottie from "react-lottie";
import animationData from "../animations/typing.json";
import { Progress } from "@chakra-ui/react";
import { ChatIcon } from '@chakra-ui/icons';

import { PhoneIcon } from '@chakra-ui/icons';


const ENDPOINT = "http://192.168.18.213:5000/";
let socket, selectedChatCompare;

const SingleChat = ({ fetchAgain, setFetchAgain }) => {


const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const [newMessage, setNewMessage] = useState("");
const [selectedFile, setSelectedFile] = useState(null);
const [fileUrl, setFileUrl] = useState("");
const [isUploading, setIsUploading] = useState(false);
const [socketConnected, setSocketConnected] = useState(false);
const [typing, setTyping] = useState(false);
const [istyping, setIsTyping] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const toast = useToast();

//...new
const [calling, setCalling] = useState(false);
const [localStream, setLocalStream] = useState(null);
const [remoteStream, setRemoteStream] = useState(null);
const [permissionDenied, setPermissionDenied] = useState(false);
const userVideoRef = useRef(null);
const remoteVideoRef = useRef(null);

const { selectedChat, setSelectedChat, user, notification, setNotification } =


ChatState();

const defaultOptions = {
loop: true,
autoplay: true,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};

//for socket get initallize first we put this use effect at the top
useEffect(() => {
socket = io(ENDPOINT);
socket.emit("setup", user);
socket.on("connected", () => setSocketConnected(true));
socket.on("incomingCall", () => handleIncomingCall());
socket.on("typing", () => setIsTyping(true));
socket.on("stop typing", () => setIsTyping(false));

return () => {
socket.disconnect();
};

}, []);

useEffect(() => {
if (calling && localStream) {
initiatePeerConnection();
}
}, [calling, localStream]);

const initiatePeerConnection = () => {


// Initialize peer connection and add local stream
const peerConnection = new RTCPeerConnection();
localStream.getTracks().forEach(track => peerConnection.addTrack(track,
localStream));
// Event handler for receiving remote stream
peerConnection.ontrack = (event) => {
setRemoteStream(event.streams[0]);
remoteVideoRef.current.srcObject = event.streams[0];
};

// Create offer and set local description


peerConnection.createOffer()
.then(offer => {
peerConnection.setLocalDescription(offer);
// Emit offer to the recipient
socket.emit("call", { recipientId: selectedChat._id, offer });
})
.catch(error => {
console.error('Error creating offer:', error);
});
};
const handleIncomingCall = () => {
setCalling(true);
};

const initiateCall = () => {


// Ask for permission to access media devices
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
setLocalStream(stream);
userVideoRef.current.srcObject = stream;
setCalling(true);
})
.catch(error => {
console.error('Error accessing media devices:', error);
setPermissionDenied(true);
toast({
title: "Permission Denied",
description: "Please allow access to your camera and microphone.",
status: "error",
duration: 5000,
isClosable: true,
});
});
};

const acceptCall = () => {


navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
setLocalStream(stream);
userVideoRef.current.srcObject = stream;
const peerConnection = new RTCPeerConnection();
stream.getTracks().forEach(track => peerConnection.addTrack(track,
stream));
peerConnection.ontrack = (event) => {
setRemoteStream(event.streams[0]);
remoteVideoRef.current.srcObject = event.streams[0];
};
peerConnection.createAnswer()
.then(answer => {
peerConnection.setLocalDescription(answer);
socket.emit("answer", { callerId: selectedChat._id, answer });
})
.catch(error => {
console.error('Error creating answer:', error);
});
})
.catch(error => {
console.error('Error accessing media devices:', error);
});
};

const rejectCall = () => {


// Handle rejecting the call
setCalling(false);
};

const fetchMessages = async () => {


if (!selectedChat) return;

try {
const config = {
headers: {
Authorization: `Bearer ${user.token}`,
},
};

setLoading(true);

const { data } = await axios.get(


`/api/message/${selectedChat._id}`,
config
);
setMessages(data);
setLoading(false);
socket.emit("join chat", selectedChat._id);
} catch (error) {
toast({
title: "Error Occured!",
description: "Failed to Load the Messages",
status: "error",
duration: 5000,
isClosable: true,
position: "bottom",
});
}
};

const typingHandler = (e) => {


setNewMessage(e.target.value);

if (!socketConnected) return;

if (!typing) {
setTyping(true);
socket.emit("typing", selectedChat._id);
}
let lastTypingTime = new Date().getTime();
let timerLength = 3000;
setTimeout(() => {
let timeNow = new Date().getTime();
let timeDiff = timeNow - lastTypingTime;
if (timeDiff >= timerLength && typing) {
socket.emit("stop typing", selectedChat._id);
setTyping(false);
}
}, timerLength);
};
const uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file);

const config = {
headers: {
'Content-type': 'multipart/form-data',
Authorization: `Bearer ${user.token}`,
},
onUploadProgress: (progressEvent) => {
// Limit the reported progress to 90%
const progress = Math.round((progressEvent.loaded * 90) /
progressEvent.total);
setUploadProgress(progress);
},
};

const { data } = await axios.post("/api/message/attachment", formData,


config);
return data.fileUrl;
};
const handleFileChange = async (e) => {
const file = e.target.files[0];
const maxSize = 10485760; // 10 MB

if (file && file.size > maxSize) {


alert('File size too large. Maximum is 10 MB.');
return;
}

setIsUploading(true);
if (file) {
setSelectedFile(file);
const fileUrl = await uploadFile(file);
setFileUrl(fileUrl);
//alert('File attached successfully! Press Enter to send the message.');
}
setIsUploading(false);
};
const sendMessage = async (event) => {
if (event.key === "Enter" && (newMessage || fileUrl)) {

try {
const config = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${user.token}`,
},
};
setNewMessage("");
setSelectedFile(null);
const { data } = await axios.post(
"/api/message",
{
content: newMessage,
chatId: selectedChat,
fileUrl: fileUrl,
},
config
);
socket.emit("new message", data);
setMessages([...messages, data]);

} catch (error) {
toast({
title: "Error Occured!",
description: "Failed to send the Message",
status: "error",
duration: 5000,
isClosable: true,
position: "bottom",
});
}
}
};

useEffect(() => {
fetchMessages();
selectedChatCompare = selectedChat;
}, [selectedChat]);

useEffect(() => {
socket.on("message recieved", (newMessageRecieved) => {
if (
!selectedChatCompare || // if chat is not selected or doesn't match
current chat
selectedChatCompare._id !== newMessageRecieved.chat._id
) {
if (!notification.includes(newMessageRecieved)) {
setNotification([newMessageRecieved, ...notification]);
setFetchAgain(!fetchAgain);
}
} else {
setMessages([...messages, newMessageRecieved]);
}
});
});
return (
<>
<video ref={userVideoRef} autoPlay muted></video>
<video ref={remoteVideoRef} autoPlay></video>

<IconButton
icon={<PhoneIcon />}
onClick={initiateCall}
position="absolute"
top="20px"
right="20px"
zIndex="9999"
isDisabled={calling || !selectedChat}
/>

{calling && (
<Box position="absolute" bottom="20px" left="50%"
transform="translateX(-50%)">
<button onClick={acceptCall} style={{ marginRight: "10px",
backgroundColor: "green", color: "white", padding: "10px 20px", border: "none",
borderRadius: "5px" }}>Accept</button>
<button onClick={rejectCall} style={{ backgroundColor: "red", color:
"white", padding: "10px 20px", border: "none", borderRadius:
"5px" }}>Reject</button>
</Box>
)}
{selectedChat ? (
<>
<Text
fontSize={{ base: "18px", md: "20px" }}
pb={3}
px={2}
w="100%"
fontFamily="Work sans"
display="flex"
justifyContent={{ base: "space-between" }}
alignItems="center"
>
<IconButton
display={{ base: "flex", md: "none" }}
icon={<ArrowBackIcon />}
onClick={() => setSelectedChat("")}
/>

{!selectedChat.isGroupChat ? (
<>
{getSender(user, selectedChat.users)}
<ProfileModal user={getSenderFull(user, selectedChat.users)} />
</>
) : (
<>
{'# '}{selectedChat.chatName}
<UpdateGroupChatModal
fetchMessages={fetchMessages}
fetchAgain={fetchAgain}
setFetchAgain={setFetchAgain}
/>
</>
)}
</Text>

<Box
display="flex"
flexDir="column"
justifyContent="flex-end"
p={3}
bg="#E8E8E8"
w="100%"
h="100%"
borderRadius="lg"
overflowY="hidden"
>
{loading ? (
<Spinner
size="xl"
w={20}
h={20}
alignSelf="center"
margin="auto"
/>
) : (
<div className="messages">
<ScrollableChat messages={messages} />
</div>
)}

<FormControl
onKeyDown={sendMessage}
id="first-name"
isRequired
mt={3}
>
{istyping ? (
<div>
<Lottie
options={defaultOptions}
width={70}
style={{ marginBottom: 15, marginLeft: 0 }}
/>
typing...
</div>
) : (
<></>
)}
<Flex direction="row" align="center">
<label htmlFor="file-upload" style={{ cursor: 'pointer',
marginRight: '10px' }}>
<i className="fas fa-paperclip"></i>
</label>
<input
id="file-upload"
type="file"
accept="image/png,image/jpeg,video/mp4"
style={{ display: 'none' }}
onChange={handleFileChange}
/>

<Input
variant="filled"
bg="#E0E0E0"
placeholder="Enter a message.."
value={newMessage}
onChange={typingHandler}
flex="1"
isDisabled={isUploading}
/>

</Flex>
{selectedFile &&
<Box ml={2} mt={2}>

<span>File Attached: </span> <span>{selectedFile.name}</span>


</Box>
}
{isUploading &&
<Box>
<Progress value={uploadProgress}
isIndeterminate={uploadProgress === 0} />
<Alert status="info">
<AlertIcon />
File is uploading...
</Alert>
</Box>
}
</FormControl>
</Box>
</>
) : (
<Box display="flex" alignItems="center" justifyContent="center" h="100%"
gap={2}>
<ChatIcon boxSize="3em" /> {/* Adjust size as needed */}
<Text fontSize="3xl" pb={3} fontFamily="Work sans" fontWeight={600}>
Start chatting on Vibex.
</Text>
</Box>
)}
</>
);
};

export default SingleChat;

backend:

const dotenv = require("dotenv")


dotenv.config();
const express = require("express");
const connectDB = require("./config/db");

// const {chats}= require("./data/data");

const usesrRoutes = require("./routes/userRoutes");


const chatRoutes = require("./routes/chatRoutes");
const messageRoutes = require("./routes/messageRoutes");
// const colors=require("colors");
const { notFound, errorHandler } = require("./middleware/errorMiddleware");
// const { Socket } = require("socket.io");

console.log(process.env.MONGO_URI);
connectDB();

const app = express();


app.use(express.json());

app.use("/api/user", usesrRoutes);
app.use("/api/chat", chatRoutes);
app.use("/api/message", messageRoutes);

//////////////////////////////////////////////////////

const path = require("path");

const __dirname1 = path.resolve();

if (process.env.NODE_ENV === "production") {


app.use(express.static(path.join(__dirname1, "/frontend/build")));

app.get("*", (req, res) =>


res.sendFile(path.resolve(__dirname1, "frontend", "build", "index.html"))
);
} else {
app.get("/", (req, res) => {
res.send("API is running..");
});
}

//////////////////////////////////////////////////////

app.use(notFound);
app.use(errorHandler);

const PORT = process.env.PORT || 5000;


const server = app.listen(PORT, console.log(`server started on PORT ${PORT}`));

const io = require("socket.io")(server, {
pingTimeout: 60000,
cors: {
origin: "http://192.168.18.213:3000"
// credentials: true,
},
});

io.on("connection", (socket) => {


console.log("Connected to socket.io");

socket.on("setup", (userData) => {


socket.join(userData._id);
socket.emit("connected");
});
socket.on("call", ({ recipientId }) => {
// Emit incoming call event to the recipient
io.to(recipientId).emit("incomingCall");
});

socket.on("answer", ({ callerId, answer }) => {


// Forward answer to the caller
io.to(callerId).emit("answer", { answer });
});
socket.on("join chat", (room) => {
socket.join(room);
// console.log("User Joined Room: " + room);
});

socket.on("new message", (newMessageRecieved) => {


var chat = newMessageRecieved.chat;

if (!chat.users) return console.log("chat.users not defined");

chat.users.forEach((user) => {
if (user._id == newMessageRecieved.sender._id) return;

socket.in(user._id).emit("message recieved", newMessageRecieved);


});
});

socket.on("typing", (room) => socket.in(room).emit("typing"));


socket.on("stop typing", (room) => socket.in(room).emit("stop typing"));

socket.off("setup", () => {
console.log("USER DISCONNECTED");
socket.leave(userData._id);
});
});
please add the video, audio call feature in my chat App in easiest way possible like we can use
zego cloud call api:
frontend:
import { FormControl } from "@chakra-ui/form-control";
import { Input } from "@chakra-ui/input";
import { Box, Text } from "@chakra-ui/layout";
import { Flex } from "@chakra-ui/layout";
import { Alert, AlertIcon } from "@chakra-ui/react";
import "./styles.css";
import { IconButton, Spinner, useToast } from "@chakra-ui/react";
import { getSender, getSenderFull } from "../config/ChatLogics";
import { useEffect, useState } from "react";
import axios from "axios";
import { ArrowBackIcon } from "@chakra-ui/icons";
import ProfileModal from "./miscellaneous/ProfileModal";
import { ChatState } from "../Context/ChatProvider";
import UpdateGroupChatModal from "./miscellaneous/UpdateGroupChatModal";
import ScrollableChat from "./ScrollableChat";
import io from "socket.io-client";
import Lottie from "react-lottie";
import animationData from "../animations/typing.json";
import { Progress } from "@chakra-ui/react";
import { ChatIcon } from '@chakra-ui/icons';
const ENDPOINT = "http://localhost:5000/";
let socket, selectedChatCompare;

const SingleChat = ({ fetchAgain, setFetchAgain }) => {


const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const [newMessage, setNewMessage] = useState("");
const [selectedFile, setSelectedFile] = useState(null);
const [fileUrl, setFileUrl] = useState("");
const [isUploading, setIsUploading] = useState(false);
const [socketConnected, setSocketConnected] = useState(false);
const [typing, setTyping] = useState(false);
const [istyping, setIsTyping] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const toast = useToast();

const { selectedChat, setSelectedChat, user, notification, setNotification } =


ChatState();

const defaultOptions = {
loop: true,
autoplay: true,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};

//for socket get initallize first we put this use effect at the top
useEffect(() => {
socket = io(ENDPOINT);
socket.emit("setup", user);
socket.on("connected", () => setSocketConnected(true));
socket.on("typing", () => setIsTyping(true));
socket.on("stop typing", () => setIsTyping(false));
}, []);

const fetchMessages = async () => {


if (!selectedChat) return;

try {
const config = {
headers: {
Authorization: `Bearer ${user.token}`,
},
};

setLoading(true);

const { data } = await axios.get(


`/api/message/${selectedChat._id}`,
config
);
setMessages(data);
setLoading(false);
socket.emit("join chat", selectedChat._id);
} catch (error) {
toast({
title: "Error Occured!",
description: "Failed to Load the Messages",
status: "error",
duration: 5000,
isClosable: true,
position: "bottom",
});
}
};

const typingHandler = (e) => {


setNewMessage(e.target.value);

if (!socketConnected) return;

if (!typing) {
setTyping(true);
socket.emit("typing", selectedChat._id);
}
let lastTypingTime = new Date().getTime();
let timerLength = 3000;
setTimeout(() => {
let timeNow = new Date().getTime();
let timeDiff = timeNow - lastTypingTime;
if (timeDiff >= timerLength && typing) {
socket.emit("stop typing", selectedChat._id);
setTyping(false);
}
}, timerLength);
};
const uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file);

const config = {
headers: {
'Content-type': 'multipart/form-data',
Authorization: `Bearer ${user.token}`,
},
onUploadProgress: (progressEvent) => {
// Limit the reported progress to 90%
const progress = Math.round((progressEvent.loaded * 90) / progressEvent.total);
setUploadProgress(progress);
},
};

const { data } = await axios.post("/api/message/attachment", formData, config);


return data.fileUrl;
};
const handleFileChange = async (e) => {
const file = e.target.files[0];
const maxSize = 10485760; // 10 MB

if (file && file.size > maxSize) {


alert('File size too large. Maximum is 10 MB.');
return;
}

setIsUploading(true);
if (file) {
setSelectedFile(file);
const fileUrl = await uploadFile(file);
setFileUrl(fileUrl);
//alert('File attached successfully! Press Enter to send the message.');
}
setIsUploading(false);
};
const sendMessage = async (event) => {
if (event.key === "Enter" && (newMessage || fileUrl)) {

try {
const config = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${user.token}`,
},
};
setNewMessage("");
setSelectedFile(null);
const { data } = await axios.post(
"/api/message",
{
content: newMessage,
chatId: selectedChat,
fileUrl: fileUrl,
},
config
);
socket.emit("new message", data);
setMessages([...messages, data]);

} catch (error) {
toast({
title: "Error Occured!",
description: "Failed to send the Message",
status: "error",
duration: 5000,
isClosable: true,
position: "bottom",
});
}
}
};

useEffect(() => {
fetchMessages();
selectedChatCompare = selectedChat;
}, [selectedChat]);

useEffect(() => {
socket.on("message recieved", (newMessageRecieved) => {
if (
!selectedChatCompare || // if chat is not selected or doesn't match current chat
selectedChatCompare._id !== newMessageRecieved.chat._id
){
if (!notification.includes(newMessageRecieved)) {
setNotification([newMessageRecieved, ...notification]);
setFetchAgain(!fetchAgain);
}
} else {
setMessages([...messages, newMessageRecieved]);
}
});
});

return (
<>
{selectedChat ? (
<>
<Text
fontSize={{ base: "18px", md: "20px" }}
pb={3}
px={2}
w="100%"
fontFamily="Work sans"
display="flex"
justifyContent={{ base: "space-between" }}
alignItems="center"
>
<IconButton
display={{ base: "flex", md: "none" }}
icon={<ArrowBackIcon />}
onClick={() => setSelectedChat("")}
/>

{!selectedChat.isGroupChat ? (
<>
{getSender(user, selectedChat.users)}
<ProfileModal user={getSenderFull(user, selectedChat.users)} />
</>
):(
<>
{'# '}{selectedChat.chatName}
<UpdateGroupChatModal
fetchMessages={fetchMessages}
fetchAgain={fetchAgain}
setFetchAgain={setFetchAgain}
/>
</>
)}
</Text>
<Box
display="flex"
flexDir="column"
justifyContent="flex-end"
p={3}
bg="#E8E8E8"
w="100%"
h="100%"
borderRadius="lg"
overflowY="hidden"
>
{loading ? (
<Spinner
size="xl"
w={20}
h={20}
alignSelf="center"
margin="auto"
/>
):(
<div className="messages">
<ScrollableChat messages={messages} />
</div>
)}

<FormControl
onKeyDown={sendMessage}
id="first-name"
isRequired
mt={3}
>
{istyping ? (
<div>
<Lottie
options={defaultOptions}
width={70}
style={{ marginBottom: 15, marginLeft: 0 }}
/>
typing...
</div>
):(
<></>
)}
<Flex direction="row" align="center">
<label htmlFor="file-upload" style={{ cursor: 'pointer', marginRight: '10px' }}>
<i className="fas fa-paperclip"></i>
</label>
<input
id="file-upload"
type="file"
accept="image/png,image/jpeg,video/mp4"
style={{ display: 'none' }}
onChange={handleFileChange}
/>

<Input
variant="filled"
bg="#E0E0E0"
placeholder="Enter a message.."
value={newMessage}
onChange={typingHandler}
flex="1"
isDisabled={isUploading}
/>

</Flex>
{selectedFile &&
<Box ml={2} mt={2}>

<span>File Attached: </span> <span>{selectedFile.name}</span>


</Box>
}
{isUploading &&
<Box>
<Progress value={uploadProgress} isIndeterminate={uploadProgress === 0} />
<Alert status="info">
<AlertIcon />
File is uploading...
</Alert>
</Box>
}
</FormControl>
</Box>
</>
):(
<Box display="flex" alignItems="center" justifyContent="center" h="100%" gap={2}>
<ChatIcon boxSize="3em" /> {/* Adjust size as needed */}
<Text fontSize="3xl" pb={3} fontFamily="Work sans" fontWeight={600}>
Start chatting on Vibex.
</Text>
</Box>
)}
</>
);
};

export default SingleChat;

backend:
const dotenv = require("dotenv")
dotenv.config();
const express = require("express");
const connectDB = require("./config/db");

// const {chats}= require("./data/data");


const usesrRoutes = require("./routes/userRoutes");
const chatRoutes = require("./routes/chatRoutes");
const messageRoutes = require("./routes/messageRoutes");

// const colors=require("colors");
const { notFound, errorHandler } = require("./middleware/errorMiddleware");
// const { Socket } = require("socket.io");

console.log(process.env.MONGO_URI);
connectDB();

const app = express();


app.use(express.json());

app.use("/api/user", usesrRoutes);
app.use("/api/chat", chatRoutes);
app.use("/api/message", messageRoutes);

//////////////////////////////////////////////////////

const path = require("path");

const __dirname1 = path.resolve();

if (process.env.NODE_ENV === "production") {


app.use(express.static(path.join(__dirname1, "/frontend/build")));

app.get("*", (req, res) =>


res.sendFile(path.resolve(__dirname1, "frontend", "build", "index.html"))
);
} else {
app.get("/", (req, res) => {
res.send("API is running..");
});
}

//////////////////////////////////////////////////////

app.use(notFound);
app.use(errorHandler);

const PORT = process.env.PORT || 5000;


const server = app.listen(PORT, console.log(`server started on PORT ${PORT}`));

const io = require("socket.io")(server, {
pingTimeout: 60000,
cors: {
origin: "http://localhost:3000"
// credentials: true,
},
});
io.on("connection", (socket) => {
console.log("Connected to socket.io");

socket.on("setup", (userData) => {


socket.join(userData._id);
socket.emit("connected");
});

socket.on("join chat", (room) => {


socket.join(room);
// console.log("User Joined Room: " + room);
});

socket.on("new message", (newMessageRecieved) => {


var chat = newMessageRecieved.chat;

if (!chat.users) return console.log("chat.users not defined");

chat.users.forEach((user) => {
if (user._id == newMessageRecieved.sender._id) return;

socket.in(user._id).emit("message recieved", newMessageRecieved);


});
});

socket.on("typing", (room) => socket.in(room).emit("typing"));


socket.on("stop typing", (room) => socket.in(room).emit("stop typing"));
socket.off("setup", () => {
console.log("USER DISCONNECTED");
socket.leave(userData._id);
});
});

You might also like