mie-webconf-app
Version:
This is a mediasoup based web conferencing app with socket.io for signalling
486 lines (427 loc) • 15.9 kB
JavaScript
// src/server.js
const express = require('express');
const http = require('http');
require('dotenv').config();
const redis = require('redis');
const yaml = require('js-yaml');
let client;
const { createWorkers, getRouter, workers } = require('./mediasoup-config');
(async () => {
await createWorkers();
})();
//environment variables
const PORT = process.env.PORT || 5001;
const BACKEND_IP = process.env.BACKEND_IP || '127.0.0.1';
const app = express();
const server = http.createServer(app);
const io = require("socket.io")(server);
// fs and path are used for parsing yaml content
const fs = require('fs');
const path = require('path')
app.use(express.static(path.join(__dirname, '../client/build')))
// Add this after the express.static middleware
// This will serve the index.html file for any route that doesn't have a file extension
// and is not an API route. This is crucial for single-page applications.
// app.get('*', (req, res) => {
// res.sendFile(path.join(__dirname, '../client/build/index.html'));
// });
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()
let producer
let consumer
let room;
const paramlist = []
let producerInfo = new Map();
let consumerInfo = new Map();
// let username;
io.on("connection", socket =>{
// console.log('new peer connected', socket.id)
socket.on('joinRoom', async ({ username, param,create }, callback) => {
socket.io = io
const roomId = param;
socket.join(roomId)
// username = username;
socket.roomId = roomId;
let router;
producerInfo.set(`${roomId}:${username}`, new Map());
consumerInfo.set(`${roomId}:${username}`, new Map());
if(client){
room = await client.exists(`room:${roomId}`);
if(create || !room){
if(room){
return callback({error: 'Please try again, room already exists'})
}
console.log('creating a new room with id:', roomId, `Adding user:${username}`)
router = await getRouter(roomId);
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);
if (room.peers.includes(username)){
console.log(`${username} already exists in room ${roomId}`)
username = `${username}-${Math.floor(Math.random() * 10)}`; // append a random number to username
}
router = await getRouter(roomId);
// console.log('router', router)
room.peers.push(username);
await client.set(`room:${roomId}`, JSON.stringify(room));
}
}}
else{
console.log('No redis client found, using device storage')
room = rooms.get(roomId)
if(!room){
console.log('creating a new room with Id:', roomId, `Adding user:${username}`)
router = await getRouter(roomId);
rooms.set(roomId, {router, peers: []})
room = rooms.get(roomId)
if(rooms.has(roomId)){
room.peers.push(username)
}
}
else{
console.log(`Adding ${username} to exising room ${roomId}`)
router = room.router
if(rooms.has(roomId)){
room = rooms.get(roomId)
room.peers.push(username)
socket.emit("newParticipant", room.peers)
console.log("emitting new participant event and sending peers", room.peers)
}
}
}
console.log('username after making it unique', username)
socket.emit('changeUsername', username)
//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())
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 using redis for storage of room data
if(client){
let roomData = await client.get(`room:${roomId}`);
room = JSON.parse(roomData)
}
const cur_peers = room.peers
const consumers = consumerInfo.get(`${roomId}:${username}`).get('consumers')
console.log("current peers in the room", cur_peers, "cur username", username)
async function createConsumers(curProducer, curPeer){
if (router.canConsume({
producerId: curProducer.id,
rtpCapabilities
})) {
// transport can now consume and return a consumer
console.log(`creating consumer for ${curPeer} in ${username}`)
consumer = await consumerInfo.get(`${roomId}:${username}`).get('consumerTransport').consume({
producerId: curProducer.id,
rtpCapabilities,
paused: true,
})
consumerInfo.get(`${roomId}:${username}`).get('consumers').set(`${curPeer}`, consumer)
console.log('consumer created for:', curPeer, consumer.id)
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", uname)
socket.to(roomId).emit('remove video', uname)
//remove peer from redis
if(client){
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")
delPeerTransports(roomId,username)
const result = await client.del(`room:${roomId}`);
console.log(result, 'result of deleting room data from redis')
io.to(roomId).emit("remove-all-videos")
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`);
}
}
// if there's no 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('peerLeft', (uname) => {
console.log('peer left:', uname, 'curuser', username)
consumerInfo.get(`${roomId}:${username}`).get('consumers').get(uname).close()
consumerInfo.get(`${roomId}:${username}`).get('consumers').delete(uname)
})
socket.on("end-meeting",async()=>{
console.log('ending the meeting in ', roomId)
let users;
if(client){
const data = await client.get(`room:${roomId}`);
if(data){
const {peers} = JSON.parse(data)
users = peers
}
}
else{
users = room.peers
}
// console.log(peers)
for(const peer of users){
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)
}
io.to(roomId).emit("remove-all-videos")
})
})
})
const createWebRtcTransport = async (router) => {
// Read config from YAML file
let transportConfig;
try {
const configPath = process.env.WEBRTC_TRANSPORT_YAML || path.join(__dirname, 'webrtc-transport.yaml');
const fileContents = fs.readFileSync(configPath, 'utf8');
const config = yaml.load(fileContents) || {};
transportConfig = config.webrtcTransport || config
} catch (err) {
console.error('Error reading webrtc-transport.yaml:', err);
// Fallback to defaults if YAML not found or invalid
transportConfig = {
listenIps: [{ ip: '0.0.0.0', announcedIp: BACKEND_IP || "127.0.0.1" }],
enableUdp: true,
enableTcp: true,
preferUdp: true,
};
}
const transport = await router.createWebRtcTransport(transportConfig);
transport.on('dtlsstatechange', dtlsState => {
if (dtlsState === 'closed') {
transport.close();
}
});
transport.on('close', () => {
console.log('Transport closed');
});
return transport;
};
const delPeerTransports = async(roomId, uname, peers) =>{
try{
console.log('deleting producers, consumers, transports for:', uname)
producerInfo.get(`${roomId}:${uname}`).get('video:producer').close();
producerInfo.get(`${roomId}:${uname}`).get('audio:producer').close();
// 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}`)
console.log("Deleted peer transports for:", uname, roomId);
}
catch(error){
console.log("Error in deleting peer transports for:", uname, roomId, error)
}
}
function startServer(){
server.listen(PORT,()=>{
console.log(`started server on port ${PORT}`)
})
// connectRedis();
}
async function stopServer() {
try{
console.log('closing server')
server.close()
}catch(e){
console.log('error while closing server', e)
}
try {
console.log('Closing all mediasoup workers');
for (const worker of workers) {
worker.close();
}
} catch (e) {
console.error('Error closing mediasoup workers:', e);
}
try {
if (client.isOpen) {
await client.quit();
}
} catch (e) {
console.error("[stopServer] Error closing Redis client:", e);
}
try{
await server.close()
}catch(e){
console.error('server closing err:',e)
}
}
// startServer()
module.exports = ({startServer, stopServer, io, client});