I am trying to create a video chat and using express
and socket.io
on the backend and react
and simple-peer
on the frontend.
This is my server code:
const http = require('http');
const express = require('express');
const app = express();
const httpServer = http.createServer(app);
app.use(require('cors')());
const io = require('socket.io')(httpServer, {
cors: {
origin: ['http://localhost:3000', 'http://localhost:3001'],
method: ['GET', 'POST']
}
});
const cache = {};
io.on('connection', socket => {
socket.on('join', id => {
cache[id] = socket.id;
console.log('cache', cache);
socket.join(id);
});
socket.on('disconnect', () => {
socket.broadcast.emit('callEnded');
});
socket.on('callUser', data => {
console.log('calling user', data.userToCall, cache[data.userToCall]);
io.to(data.userToCall).emit('callUser', {
signal: data.signalData,
from: data.from,
name: data.name
});
});
// socket.on('answerCall', data => {
// console.log('data', data);
// io.to(cache[data.to]).emit('callAccepted', data.signal);
// });
socket.on('answerCall', data => {
console.log('answering call', data);
io.to(data.to).emit('callAccepted', data.signal);
});
});
httpServer.listen(4000, () => 'Listening...');
I am accepting requests from port 3000 and 3001 because that is where I am running my two apps. As I don't have a login system on the backend at the moment, I am running one app for each peer.
The code for the first app looks like this:
import { useEffect, useState, useRef } from "react";
import Peer from "simple-peer";
import io from "socket.io-client";
import './App.css';
const ID = 'fey'
function App() {
const [ stream, setStream ] = useState()
const [receivingCall, setReceivingCall] = useState(false) ;
const [caller, setCaller] = useState("") ;
const [callerSignal, setCallerSignal] = useState() ;
const [callAccepted, setCallAccepted] = useState(false);
const [callEnded, setCallEnded] = useState(false);
const socket = io.connect("http://localhost:4000/");
const myVideo = useRef()
const userVideo = useRef()
const connectionRef = useRef()
useEffect(() => {
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then((stream)=>{
setStream(stream)
myVideo.current.srcObject = stream
})
.catch((err) => console.log(err))
socket.emit('join', ID)
socket.on("callUser", (data)=>{
setReceivingCall(true)
setCaller(data.from)
setCallerSignal(data.signal)
})
}, [])
const callUser = ()=> {
const peer = new Peer({
initiator:true,
trickle:false,
stream:stream
})
peer.on("signal", (data)=>{
socket.emit("callUser",{
userToCall: 'fey-clone',
signalData: data,
from: ID,
name: "Fey"
})
})
peer.on("stream", (stream)=> {
userVideo.current.srcObject = stream
})
socket.on("callAccepted", (signal) => {
console.log('call accepted!!')
setCallAccepted(true)
peer.signal(signal)
})
connectionRef.current = peer
}
const answerCall = () => {
console.log('peer exists')
setCallAccepted(true)
const peer = new Peer({
initiator:false,
trickle:false,
stream: stream
})
peer.on("signal", (data)=> {
console.log('call answered')
socket.emit("answerCall", {signal:data, to: caller})
})
peer.on("stream", (stream) =>{
userVideo.current.srcObject = stream
})
peer.signal(callerSignal)
connectionRef.current = peer
}
const leaveCall = ()=>{
setCallEnded(true)
connectionRef.current.destroy()
}
return (
<div className="App">
<div className="video">
{stream && <video playsInline muted ref={myVideo} autoPlay style={{width: "300px", height: "300px" }} />}
{callAccepted && <video playsInline muted ref={userVideo} autoPlay style={{width: "300px", height: "300px" }} />}
</div>
{callAccepted && !callEnded ? (
<button onClick={leaveCall}>
End Call
</button>
):(
<button onClick={callUser}>call meeee</button>
)}
{receivingCall && !callAccepted ? (
<div className="caller">
<h1>Fey is calling ...</h1>
<button onClick={answerCall} >
Answer
</button>
</div>
) : null
}
</div>
);
}
export default App;
The code for the other peer looks unsurprisingly similar, but the ID is different and the userToCall
is the other one.
import { useEffect, useState, useRef } from "react";
import Peer from "simple-peer";
import io from "socket.io-client";
import './App.css';
const ID = 'fey-clone'
function App() {
const [ stream, setStream ] = useState()
const [receivingCall, setReceivingCall] = useState(false) ;
const [caller, setCaller] = useState("") ;
const [callerSignal, setCallerSignal] = useState() ;
const [callAccepted, setCallAccepted] = useState(false);
const [callEnded, setCallEnded] = useState(false);
const socket = io.connect("http://localhost:4000/");
const myVideo = useRef()
const userVideo = useRef()
const connectionRef = useRef()
useEffect(() => {
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then((stream)=>{
setStream(stream)
myVideo.current.srcObject = stream
})
.catch((err) => console.log(err))
socket.emit('join', ID)
socket.on("callUser", (data)=>{
setReceivingCall(true)
setCaller(data.from)
setCallerSignal(data.signal)
})
}, [])
const callUser = ()=> {
const peer = new Peer({
initiator:true,
trickle:false,
stream:stream
})
peer.on("signal", (data)=>{
socket.emit("callUser",{
userToCall: 'fey',
signalData: data,
from: ID,
name: "fey clone"
})
})
peer.on("stream", (stream)=> {
userVideo.current.srcObject = stream
})
socket.on("callAccepted", (signal) => {
console.log('call accepted!!')
setCallAccepted(true)
peer.signal(signal)
})
connectionRef.current = peer
}
const answerCall = () => {
setCallAccepted(true)
const peer = new Peer({
initiator:false,
trickle:false,
stream: stream
})
peer.on("signal", (data)=> {
console.log(data, caller)
socket.emit("answerCall", {signal:data, to: caller})
})
peer.on("stream", (stream) =>{
console.log('I streeeam')
userVideo.current.srcObject = stream
})
peer.signal(callerSignal)
connectionRef.current = peer
}
const leaveCall = ()=>{
setCallEnded(true)
connectionRef.current.destroy()
}
return (
<div className="App">
<div className="video">
{stream && <video playsInline muted ref={myVideo} autoPlay style={{width: "300px", height: "300px" }} />}
{callAccepted && <video playsInline muted ref={userVideo} autoPlay style={{width: "300px", height: "300px" }} />}
</div>
{callAccepted && !callEnded ? (
<button onClick={leaveCall}>
End Call
</button>
):(
<button onClick={callUser}>call meeee</button>
)}
{receivingCall && !callAccepted ? (
<div className="caller">
<h1>Fey is calling ...</h1>
<button onClick={answerCall} >
Answer
</button>
</div>
) : null
}
</div>
);
}
export default App;
The call seems to be going through the right recipient. When fey
calls fey-clone
, fey-clone
can accept the call. The signal
seems to work fine as well. However, it seems that the original caller fey
never receives the event callAccepted
from the server, so the video call cannot start. It appears with most probability that the server does not emit the event to the right peer, but I tried to debug to no avail. Is there something I am missing here?
CodePudding user response:
It looks like the problem lies on the connection from the client. The connection is happening inside the component, which means a new connection will be created every time the component re-renders. Bad mistake.
My code on the client looks like this now:
import React,{ useEffect, useRef, useState } from "react";
import Peer from "simple-peer";
import io from "socket.io-client";
const socket = io.connect('http://localhost:5000')
function Chat() {
const [ stream, setStream ] = useState()
const [ receivingCall, setReceivingCall ] = useState(false)
const [ caller, setCaller ] = useState("")
const [ callerSignal, setCallerSignal ] = useState()
const [ callAccepted, setCallAccepted ] = useState(false)
const [ idToCall, setIdToCall ] = useState("")
const [ callEnded, setCallEnded] = useState(false)
const [ name, setName ] = useState("")
const myVideo = useRef()
const userVideo = useRef()
const connectionRef= useRef()
useEffect(() => {
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then((stream) => {
setStream(stream)
myVideo.current.srcObject = stream
})
socket.emit('join', 'fey')
socket.on("callUser", (data) => {
setReceivingCall(true)
setCaller(data.from)
setName(data.name)
setCallerSignal(data.signal)
})
}, [])
const callUser = (id) => {
const peer = new Peer({
initiator: true,
trickle: false,
stream: stream
})
peer.on("signal", (data) => {
socket.emit("callUser", {
userToCall: 'fey-clone',
signalData: data,
from: 'fey',
name: 'fey'
})
})
peer.on("stream", (stream) => {
userVideo.current.srcObject = stream
})
socket.on("callAccepted", (signal) => {
setCallAccepted(true)
peer.signal(signal)
})
connectionRef.current = peer
}
const answerCall =() => {
setCallAccepted(true)
const peer = new Peer({
initiator: false,
trickle: false,
stream: stream
})
peer.on("signal", (data) => {
socket.emit("answerCall", { signal: data, to: caller })
})
peer.on("stream", (stream) => {
userVideo.current.srcObject = stream
})
peer.signal(callerSignal)
connectionRef.current = peer
}
const leaveCall = () => {
setCallEnded(true)
connectionRef.current.destroy()
}
return (
<>
<h1>zoomish</h1>
<div className="container">
<div className="video-container">
<div className="video">
{stream && <video playsInline muted ref={myVideo} autoPlay style={{width: "300px" }} />}
</div>
<div className="video">
{callAccepted && !callEnded ?
<video playsInline ref={userVideo} autoPlay style={{width:"300px"}}/>
: null}
</div>
</div>
<div className="myId">
<div className="call-button">
{callAccepted && !callEnded ? (
<button onClick={leaveCall}>End Call</button>
):(
<button onClick={()=>callUser(idToCall)}>call me</button>
)}
</div>
</div>
<div>
{receivingCall && !callAccepted ? (
<div className="caller">
<h1>{name} is calling ...</h1>
<button onClick={answerCall}>Answer</button>
</div>
):null}
</div>
</div>
</>
);
}
export default Chat;
On the server nothing is particularly wrong, but I replicating the room that is already assigned to a socket once it connects with socket.join
, and that does not need to be done. So I moved to maintaining a map of userId
and socket.id
. In a production app, this would probably be delegate to a separate storage for better handling the traffic. My server looks like this now:
const express = require("express")
const http = require("http")
const app = express()
const server = http.createServer(app)
const io = require("socket.io")(server, {
cors: {
origin: ["http://localhost:3000", "http://localhost:3001" ],
methods: [ "GET", "POST" ]
}
})
let users = {}
io.on("connection", (socket) => {
socket.on('join', (userId) => {
users[userId] = socket.id
});
socket.on("disconnect", () => {
socket.broadcast.emit("callEnded")
})
socket.on("callUser", (data) => {
io.to(users[data.userToCall]).emit("callUser", {
signal: data.signalData,
from: data.from,
name: data.name
})
})
socket.on("answerCall", (data) => {
io.to(users[data.to]).emit("callAccepted", data.signal)
})
})
server.listen(5000, () => console.log("server is running on port 5000"))
And that's it!