mie-webconf-app
Version:
This is a mediasoup based web conferencing app with socket.io for signalling
429 lines (391 loc) • 14.8 kB
JavaScript
// src/server.js
const express = require('express');
const http = require('http');
// const { mediasoup } = require('mediasoup');
const { Server } = require('socket.io');
const cors = require('cors')
const path = require('path')
require('dotenv').config();
const redis = require('redis');
const { createWorker, getRouter } = require('./mediasoup-config');
(async () => {
await createWorker();
})();
const app = express();
const server = http.createServer(app);
const io = new Server(server);
let client;
//serve frontend build
app.use(express.static(path.join(__dirname, '../client/build')));
// cors setup
// app.use(cors({
// origin: "http://localhost:3000", // Allow React frontend
// credentials: true // Allow cookies & authentication headers
// }));
// app.use((req, res, next) => {
// res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
// res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
// res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
// res.setHeader("Access-Control-Allow-Credentials", "true");
// if (req.method === "OPTIONS") {
// return res.sendStatus(200);
// }
// next();
// });
function validateEnv() {
const requiredVars = ['REDIS_HOST', 'REDIS_PORT', 'REDIS_PASSWORD'];
const missing = requiredVars.filter((key) => !process.env[key]);
if (missing.length > 0) {
console.log(`Missing required environment variables: ${missing.join(', ')}`)
console.log('using device storage instead')
return false
}
return true
}
// Call before using Redis
if (validateEnv()){
client = redis.createClient({
username: 'default',
password: process.env.REDIS_PASSWORD,
socket: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
}
});
(async () => {
await client.connect();
console.log('Connected to Redis');
})();
client.on('error', err => console.log('Redis Client Error', err));
}
const rooms = new Map()
// const peers = io.of('/mediasoup')
let producer
let consumer
let room;
const paramlist = []
let producerInfo = new Map();
let consumerInfo = new Map();
io.on("connection", socket =>{
console.log('new peer connected', socket.id)
socket.on('joinRoom', async ({ username, roomId }, callback) => {
socket.io = io
socket.join(roomId)
let router;
producerInfo.set(`${roomId}:${username}`, new Map());
consumerInfo.set(`${roomId}:${username}`, new Map());
if (client){
room = await client.exists(`room:${roomId}`);
if(!room){
console.log('creating a new room with id:', roomId, `Adding user:${username}`)
router = await getRouter(roomId);
// console.log('router',router)
if(router){
const roomData = {
peers: [username]
};
await client.set(`room:${roomId}`, JSON.stringify(roomData));
}
}
else{
console.log(`Adding ${username} to existing room ${roomId}`)
const data = await client.get(`room:${roomId}`);
console.log(data, 'exiting room data')
if (data){
room = JSON.parse(data);
router = await getRouter(roomId);
// console.log('router', router)
room.peers.push(username);
socket.emit("newParticipant", room.peers)
console.log("emiting event new participant and sending peers:", room.peers)
await client.set(`room:${roomId}`, JSON.stringify(room));
}
}
}else{
room = rooms.get(roomId)
if(!room){
console.log('creating a new room with id:', roomId, `Adding user:${username}`)
router = await getRouter();
rooms.set(roomId, { router, peers: [] });
room = rooms.get(roomId)
if(rooms.has(roomId)){
room.peers.push(username)
}
}
else{
console.log(`Adding ${username} to existing room ${roomId}`)
router = room.router
if(rooms.has(roomId)){
room = rooms.get(roomId)
room.peers.push(username)
socket.emit("newParticipant", room.peers)
console.log("emiting event new participant and sending peers:", room.peers)
}
}
}
//send router rtpcapabilities to client
if(router){
const rtpCapabilities = await router.rtpCapabilities
// console.log('rtp',rtpCapabilities)
callback({rtpCapabilities})
}
//once we have the router, we create produce and consume transports for each
socket.on("createWebRTCTransport",async(callback)=>{
// create both producerTransport and consumer Transport
if(!router){
console.log('Failed to fetch router for this room')
return
}
let producerTransport = await createWebRtcTransport(router)
let consumerTransport = await createWebRtcTransport(router)
producerInfo.get(`${roomId}:${username}`).set('producerTransport', producerTransport);
consumerInfo.get(`${roomId}:${username}`).set('consumerTransport', consumerTransport)
consumerInfo.get(`${roomId}:${username}`).set('consumers', new Map())
// transports.set(`${username}`, { producer: producerTransport, consumer: consumerTransport})
// console.log('transports for current user stored in map')
// await client.set(`producers:${roomId}${username}`,[])
const producerOptions = {
id: producerTransport.id,
iceParameters: producerTransport.iceParameters,
iceCandidates: producerTransport.iceCandidates,
dtlsParameters: producerTransport.dtlsParameters
}
const consumerOptions = {
id: consumerTransport.id,
iceParameters: consumerTransport.iceParameters,
iceCandidates: consumerTransport.iceCandidates,
dtlsParameters: consumerTransport.dtlsParameters
}
callback({producer:producerOptions,consumer:consumerOptions})
// console.log('transports created')
// router.observer.on("newtransport", ()=>{
// console.log("new transport created") //not working when the first user transports are created
// io.in(roomId).emit("new-transport", username)
// })
})
// see client's socket.emit('transport-connect', ...)
socket.on('transport-connect', async ({ dtlsParameters }) => {
// console.log('DTLS PARAMS... ', { dtlsParameters },producerTransport)
await producerInfo.get(`${roomId}:${username}`).get('producerTransport').connect({ dtlsParameters })
console.log('producer connected')
})
// see client's socket.emit('transport-produce', ...)
socket.on('transport-produce', async ({ kind, rtpParameters, appData }, callback) => {
// call produce based on the prameters from the client
console.log('producer producing', kind)
producer = await producerInfo.get(`${roomId}:${username}`).get('producerTransport').produce({
kind,
rtpParameters,
})
producerInfo.get(`${roomId}:${username}`).set(`${kind}:producer`, producer)
// console.log('producer from map', producerInfo)
io.to(roomId).emit("new-transport", username)
producer.on('transportclose', () => {
console.log('transport for this producer closed ')
producer.close()
})
// Send back to the client the Producer's id
callback({
id: producer.id
})
})
// see client's socket.emit('transport-recv-connect', ...)
socket.on('transport-recv-connect', async ({ dtlsParameters }) => {
// console.log(`DTLS PARAMS: ${dtlsParameters}`)
const consumer = await consumerInfo.get(`${roomId}:${username}`).get('consumerTransport');
await consumer.connect({ dtlsParameters })
})
socket.on('consume', async ({ rtpCapabilities }, callback) => {
const paramsList = []
try {
// check if the router can consume the specified producer
if(client){
let roomData = await client.get(`room:${roomId}`);
room = JSON.parse(roomData)
}
const cur_peers = room.peers
console.log("current peers in the room", cur_peers, "cur username", username)
const consumers = consumerInfo.get(`${roomId}:${username}`).get('consumers')
async function createConsumers(curProducer, curPeer){
if (router.canConsume({
producerId: curProducer.id,
rtpCapabilities
})) {
// transport can now consume and return a consumer
console.log('router can consume', 'creating consumer for:', curPeer)
consumer = await consumerInfo.get(`${roomId}:${username}`).get('consumerTransport').consume({
producerId: curProducer.id,
rtpCapabilities,
paused: true,
})
consumerInfo.get(`${roomId}:${username}`).get('consumers').set(`${curPeer}`, consumer)
consumer.on('transportclose', () => {
console.log('transport close for consumer')
})
consumer.on('producerclose', () => {
console.log('producer of consumer closed')
})
// from the consumer extract the following params
// to send back to the Client
let params = {
user: curPeer,
id: consumer.id,
producerId: producer.id,
kind: consumer.kind,
rtpParameters: consumer.rtpParameters,
resumed:false
}
paramsList.push(params);
}
}
for(const peer of cur_peers){
if(peer!=username){
if (!consumers.has(peer)){
console.log(`${peer} doesn't have a counsumer in ${username}`)
// console.log('producerInfo', producerInfo)
let audioProducer = await producerInfo.get(`${roomId}:${peer}`).get('audio:producer')
let videoProducer = await producerInfo.get(`${roomId}:${peer}`).get('video:producer')
// console.log('producer for ',peer, producer)
if (audioProducer) {
await createConsumers(audioProducer, peer)
}
if (videoProducer) {
await createConsumers(videoProducer, peer)
}
}
}
}
// console.log('paramsList after callback', paramsList)
callback({paramsList:paramsList})
} catch (error) {
paramsList.push({ error: `Could not consume: ${error.message}` });
console.log(error.message)
callback({paramsList});
}
})
socket.on('consumer-resume', async (user, consumerId) => {
console.log('consumer resume')
consumerInfo.get(`${roomId}:${username}`).get('consumers').get(user).resume()
})
socket.on('hangup', async(uname) =>{
console.log("on one peer left")
socket.to(roomId).emit('remove video', uname)
delPeerTransports(roomId, uname)
if(client){
//remove peer from redis
const roomKey = `room:${roomId}`
const data = await client.get(roomKey);
if (data) {
const roomData = JSON.parse(data);
// Remove the peer from the array
roomData.peers = roomData.peers.filter(peer => peer !== uname);
console.log('filetered peers', roomData.peers)
if(roomData.peers.length===1){
// io.to(roomId).emit("end-meeting")
io.to(roomId).emit("remove video", roomData.peers[0])
delPeerTransports(roomId,username)
const result = await client.del(`room:${roomId}`);
console.log(result, 'result of deleting room data from redis')
//close router
router.close()
return
}
// Update the Redis entry
await client.set(roomKey, JSON.stringify(roomData));
console.log(`Removed ${uname} from room ${roomId}`);
} else {
console.log(`Room ${roomId} or ${uname} not found in Redis`);
}
} else{
room.peers = room.peers.filter(peer => peer !== uname);
// Optionally remove the room if it's empty
if (room.peers.length === 1) {
io.to(roomId).emit("remove video", room.peers[0])
delPeerTransports(roomId,username)
rooms.delete(roomId);
router.close()
return
} else {
rooms.set(roomId, room); // update Map (technically not needed unless replacing the object)
}
}
})
socket.on("end-meeting",async()=>{
io.to(roomId).emit("remove-all-videos")
console.log('ending the meeting in ', roomId)
let peers;
if(client){
const data = await client.get(`room:${roomId}`);
if(data){
const {peers} = JSON.parse(data)
peers = peers
}
}
else{
peers = room.peers
}
// console.log(peers)
for(const peer of peers){
console.log('About to delete peer transports for:', roomId, peer, 'inside end meet for loop')
delPeerTransports(roomId, peer)
}
if(client){
const result = await client.del(`room:${roomId}`);
console.log(result, 'result of deleting room data from redis')
}else{
rooms.delete(roomId)
}
//close router
router.close()
})
})
})
const createWebRtcTransport = async (router) => {
const transport = await router.createWebRtcTransport({
listenIps: [{ ip: '127.0.0.1', announcedIp: '127.0.0.1' }],
enableUdp: true,
enableTcp: true,
preferUdp: true,
});
transport.on('dtlsstatechange', dtlsState => {
if (dtlsState === 'closed') {
transport.close();
}
});
transport.on('close', () => {
console.log('Transport closed')
});
return transport;
};
const delPeerTransports = async(roomId, uname) =>{
try{
console.log('deleting producers, consumers, transports for:', uname)
await producerInfo.get(`${roomId}:${uname}`).get('video:producer').close();
await producerInfo.get(`${roomId}:${uname}`).get('audio:producer').close();
const userConsumers = consumerInfo.get(`${roomId}:${uname}`).get('consumers') || {};
// Close all consumers
for (const peerId in userConsumers) {
for (const consumer of Object.values(userConsumers[peerId])) {
try {
consumer.close();
} catch (err) {
console.error("Error closing consumer", err);
}
}
}
console.log(producerInfo.get(`${roomId}:${uname}`))
producerInfo.get(`${roomId}:${uname}`).get('producerTransport').close();
consumerInfo.get(`${roomId}:${uname}`).get('consumerTransport').close();
producerInfo.delete(`${roomId}:${uname}`)
consumerInfo.delete(`${roomId}:${uname}`)
}
catch(error){
console.log("Error in deleting peer transports for:", uname, roomId, error)
}
}
function startServer() {
server.listen(5001, () => {
console.log('Server is running on http://localhost:5001');
});
}
module.exports = { startServer };