diff --git a/app.js b/app.js index 860dae3..b1c258f 100644 --- a/app.js +++ b/app.js @@ -1,299 +1,301 @@ -import 'dotenv/config' - -/** - * integrating mediasoup server with a node.js application - */ - -/* Please follow mediasoup installation requirements */ -/* https://mediasoup.org/documentation/v3/mediasoup/installation/ */ -import express from 'express' -const app = express() - -import https from 'httpolyglot' -import fs from 'fs' -import path from 'path' -const __dirname = path.resolve() - -import Server from 'socket.io' -import mediasoup, { getSupportedRtpCapabilities } from 'mediasoup' - -let worker -let router = {} -let producerTransport -let consumerTransport -let producer -let consumer - -app.get('/', (_req, res) => { - res.send('Hello from mediasoup app!') -}) - -app.use('/sfu', express.static(path.join(__dirname, 'public'))) - -// SSL cert for HTTPS access -const options = { - key: fs.readFileSync('./server/ssl/key.pem', 'utf-8'), - cert: fs.readFileSync('./server/ssl/cert.pem', 'utf-8') -} - -const httpsServer = https.createServer(options, app) - -httpsServer.listen(process.env.PORT, () => { - console.log('Listening on port:', process.env.PORT) -}) - -const io = new Server(httpsServer) - -// socket.io namespace (could represent a room?) -const peers = io.of('/mediasoup') - -/** - * Worker - * |-> Router(s) - * |-> Producer Transport(s) - * |-> Producer - * |-> Consumer Transport(s) - * |-> Consumer - **/ - -const createWorker = async () => { - worker = await mediasoup.createWorker({ - rtcMinPort: 2000, - rtcMaxPort: 2020, - }) - console.log(`[createWorker] worker pid ${worker.pid}`) - - worker.on('died', error => { - // This implies something serious happened, so kill the application - console.error('mediasoup worker has died', error) - setTimeout(() => process.exit(1), 2000) // exit in 2 seconds - }) - - return worker -} - -// We create a Worker as soon as our application starts -worker = createWorker() - -// This is an Array of RtpCapabilities -// https://mediasoup.org/documentation/v3/mediasoup/rtp-parameters-and-capabilities/#RtpCodecCapability -// list of media codecs supported by mediasoup ... -// https://github.com/versatica/mediasoup/blob/v3/src/supportedRtpCapabilities.ts -const mediaCodecs = [ - { - kind: 'audio', - mimeType: 'audio/opus', - clockRate: 48000, - channels: 2, - }, - { - kind: 'video', - mimeType: 'video/VP8', - clockRate: 90000, - parameters: { - 'x-google-start-bitrate': 1000, - }, - }, -] - -peers.on('connection', async socket => { - console.log('[connection] socketId:', socket.id) - socket.emit('connection-success', { - socketId: socket.id, - existsProducer: producer ? true : false, - }) - - socket.on('disconnect', () => { - // do some cleanup - console.log('peer disconnected') - }) - - socket.on('createRoom', async ({ callId }, callback) => { - console.log('[createRoom] callId', callId); - console.log('Router length:', Object.keys(router).length); - if (router[callId] === undefined) { - // worker.createRouter(options) - // options = { mediaCodecs, appData } - // mediaCodecs -> defined above - // appData -> custom application data - we are not supplying any - // none of the two are required - router[callId] = await worker.createRouter({ mediaCodecs }) - console.log(`[createRoom] Router ID: ${router[callId].id}`) - } - - getRtpCapabilities(callId, callback) - }) - - const getRtpCapabilities = (callId, callback) => { - const rtpCapabilities = router[callId].rtpCapabilities - - callback({ rtpCapabilities }) - } - - // Client emits a request to create server side Transport - // We need to differentiate between the producer and consumer transports - socket.on('createWebRtcTransport', async ({ sender, callId }, callback) => { - console.log(`[createWebRtcTransport] Is this a sender request? ${sender} | callId ${callId}`) - // The client indicates if it is a producer or a consumer - // if sender is true, indicates a producer else a consumer - if (sender) - producerTransport = await createWebRtcTransportLayer(callId, callback) - else - consumerTransport = await createWebRtcTransportLayer(callId, callback) - }) - - // see client's socket.emit('transport-connect', ...) - socket.on('transport-connect', async ({ dtlsParameters }) => { - console.log('[transport-connect] DTLS PARAMS... ', { dtlsParameters }) - await producerTransport.connect({ dtlsParameters }) - }) - - // see client's socket.emit('transport-produce', ...) - socket.on('transport-produce', async ({ kind, rtpParameters, appData }) => { - // call produce based on the prameters from the client - producer = await producerTransport.produce({ - kind, - rtpParameters, - }) - console.log(`[transport-produce] Producer ID: ${producer.id} | kind: ${producer.kind}`) - - producer.on('transportclose', () => { - console.log('transport for this producer closed', callId) - - // https://mediasoup.org/documentation/v3/mediasoup/api/#producer-close - producer.close() - - // https://mediasoup.org/documentation/v3/mediasoup/api/#router-close - router[callId].close() - delete router[callId] - }) - }) - - // see client's socket.emit('transport-recv-connect', ...) - socket.on('transport-recv-connect', async ({ dtlsParameters }) => { - console.log(`[transport-recv-connect] DTLS PARAMS: ${dtlsParameters}`) - await consumerTransport.connect({ dtlsParameters }) - }) - - socket.on('consume', async ({ rtpCapabilities, callId }, callback) => { - try { - // console.log('consume', rtpCapabilities, callId); - // check if the router can consume the specified producer - if (router[callId].canConsume({ - producerId: producer.id, - rtpCapabilities - })) { - // transport can now consume and return a consumer - consumer = await consumerTransport.consume({ - producerId: producer.id, - rtpCapabilities, - paused: true, - }) - - consumer.on('transportclose', () => { - console.log('transport close from consumer', callId) - - // https://mediasoup.org/documentation/v3/mediasoup/api/#router-close - router[callId].close() - delete router[callId] - producer.close() - consumer.close() - }) - - consumer.on('producerclose', () => { - console.log('producer of consumer closed', callId) - - // https://mediasoup.org/documentation/v3/mediasoup/api/#router-close - router[callId].close() - delete router[callId] - producer.close() - consumer.close() - }) - - // from the consumer extract the following params - // to send back to the Client - const params = { - id: consumer.id, - producerId: producer.id, - kind: consumer.kind, - rtpParameters: consumer.rtpParameters, - } - - // send the parameters to the client - callback({ params }) - } - } catch (error) { - console.log(error.message) - callback({ - params: { - error: error - } - }) - } - }) - - socket.on('consumer-resume', async () => { - console.log(`[consumer-resume]`) - await consumer.resume() - }) -}) - -const createWebRtcTransportLayer = async (callId, callback) => { - try { - console.log('[createWebRtcTransportLayer] callId', callId); - // https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions - const webRtcTransport_options = { - listenIps: [ - { - ip: process.env.IP, // Listening IPv4 or IPv6. - announcedIp: process.env.ANNOUNCED_IP, // Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP). - } - ], - enableUdp: true, - enableTcp: true, - preferUdp: true, - } - - // console.log('webRtcTransport_options', webRtcTransport_options); - // console.log('router', router, '| router[callId]', router[callId]); - - // https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport - let transport = await router[callId].createWebRtcTransport(webRtcTransport_options) - console.log(`callId: ${callId} | transport id: ${transport.id}`) - - transport.on('dtlsstatechange', dtlsState => { - if (dtlsState === 'closed') { - transport.close() - } - }) - - transport.on('close', () => { - console.log('transport closed') - }) - - const params = { - id: transport.id, - iceParameters: transport.iceParameters, - iceCandidates: transport.iceCandidates, - dtlsParameters: transport.dtlsParameters, - } - - // console.log('params', params); - - // send back to the client the following prameters - callback({ - // https://mediasoup.org/documentation/v3/mediasoup-client/api/#TransportOptions - params - }) - - return transport - - } catch (error) { - console.log('[createWebRtcTransportLayer] ERROR', JSON.stringify(error)); - callback({ - params: { - error: error - } - }) - } +import 'dotenv/config' + +/** + * integrating mediasoup server with a node.js application + */ + +/* Please follow mediasoup installation requirements */ +/* https://mediasoup.org/documentation/v3/mediasoup/installation/ */ +import express from 'express' +const app = express() + +import https from 'httpolyglot' +import fs from 'fs' +import path from 'path' +const __dirname = path.resolve() + +import Server from 'socket.io' +import mediasoup, { getSupportedRtpCapabilities } from 'mediasoup' + +let worker +let router = {} +let producerTransport +let consumerTransport +let producer +let consumer + +app.get('/', (_req, res) => { + res.send('Hello from mediasoup app!') +}) + +app.use('/sfu', express.static(path.join(__dirname, 'public'))) + +// SSL cert for HTTPS access +const options = { + key: fs.readFileSync('./server/ssl/key.pem', 'utf-8'), + cert: fs.readFileSync('./server/ssl/cert.pem', 'utf-8') +} + +const httpsServer = https.createServer(options, app) + +httpsServer.listen(process.env.PORT, () => { + console.log('Listening on port:', process.env.PORT) +}) + +const io = new Server(httpServer, { + allowEIO3: true +}); + +// socket.io namespace (could represent a room?) +const peers = io.of('/mediasoup') + +/** + * Worker + * |-> Router(s) + * |-> Producer Transport(s) + * |-> Producer + * |-> Consumer Transport(s) + * |-> Consumer + **/ + +const createWorker = async () => { + worker = await mediasoup.createWorker({ + rtcMinPort: 2000, + rtcMaxPort: 2020, + }) + console.log(`[createWorker] worker pid ${worker.pid}`) + + worker.on('died', error => { + // This implies something serious happened, so kill the application + console.error('mediasoup worker has died', error) + setTimeout(() => process.exit(1), 2000) // exit in 2 seconds + }) + + return worker +} + +// We create a Worker as soon as our application starts +worker = createWorker() + +// This is an Array of RtpCapabilities +// https://mediasoup.org/documentation/v3/mediasoup/rtp-parameters-and-capabilities/#RtpCodecCapability +// list of media codecs supported by mediasoup ... +// https://github.com/versatica/mediasoup/blob/v3/src/supportedRtpCapabilities.ts +const mediaCodecs = [ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + }, + { + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + parameters: { + 'x-google-start-bitrate': 1000, + }, + }, +] + +peers.on('connection', async socket => { + console.log('[connection] socketId:', socket.id) + socket.emit('connection-success', { + socketId: socket.id, + existsProducer: producer ? true : false, + }) + + socket.on('disconnect', () => { + // do some cleanup + console.log('peer disconnected') + }) + + socket.on('createRoom', async ({ callId }, callback) => { + console.log('[createRoom] callId', callId); + console.log('Router length:', Object.keys(router).length); + if (router[callId] === undefined) { + // worker.createRouter(options) + // options = { mediaCodecs, appData } + // mediaCodecs -> defined above + // appData -> custom application data - we are not supplying any + // none of the two are required + router[callId] = await worker.createRouter({ mediaCodecs }) + console.log(`[createRoom] Router ID: ${router[callId].id}`) + } + + getRtpCapabilities(callId, callback) + }) + + const getRtpCapabilities = (callId, callback) => { + const rtpCapabilities = router[callId].rtpCapabilities + + callback({ rtpCapabilities }) + } + + // Client emits a request to create server side Transport + // We need to differentiate between the producer and consumer transports + socket.on('createWebRtcTransport', async ({ sender, callId }, callback) => { + console.log(`[createWebRtcTransport] Is this a sender request? ${sender} | callId ${callId}`) + // The client indicates if it is a producer or a consumer + // if sender is true, indicates a producer else a consumer + if (sender) + producerTransport = await createWebRtcTransportLayer(callId, callback) + else + consumerTransport = await createWebRtcTransportLayer(callId, callback) + }) + + // see client's socket.emit('transport-connect', ...) + socket.on('transport-connect', async ({ dtlsParameters }) => { + console.log('[transport-connect] DTLS PARAMS... ', { dtlsParameters }) + await producerTransport.connect({ dtlsParameters }) + }) + + // see client's socket.emit('transport-produce', ...) + socket.on('transport-produce', async ({ kind, rtpParameters, appData }) => { + // call produce based on the prameters from the client + producer = await producerTransport.produce({ + kind, + rtpParameters, + }) + console.log(`[transport-produce] Producer ID: ${producer.id} | kind: ${producer.kind}`) + + producer.on('transportclose', () => { + console.log('transport for this producer closed', callId) + + // https://mediasoup.org/documentation/v3/mediasoup/api/#producer-close + producer.close() + + // https://mediasoup.org/documentation/v3/mediasoup/api/#router-close + router[callId].close() + delete router[callId] + }) + }) + + // see client's socket.emit('transport-recv-connect', ...) + socket.on('transport-recv-connect', async ({ dtlsParameters }) => { + console.log(`[transport-recv-connect] DTLS PARAMS: ${dtlsParameters}`) + await consumerTransport.connect({ dtlsParameters }) + }) + + socket.on('consume', async ({ rtpCapabilities, callId }, callback) => { + try { + // console.log('consume', rtpCapabilities, callId); + // check if the router can consume the specified producer + if (router[callId].canConsume({ + producerId: producer.id, + rtpCapabilities + })) { + // transport can now consume and return a consumer + consumer = await consumerTransport.consume({ + producerId: producer.id, + rtpCapabilities, + paused: true, + }) + + consumer.on('transportclose', () => { + console.log('transport close from consumer', callId) + + // https://mediasoup.org/documentation/v3/mediasoup/api/#router-close + router[callId].close() + delete router[callId] + producer.close() + consumer.close() + }) + + consumer.on('producerclose', () => { + console.log('producer of consumer closed', callId) + + // https://mediasoup.org/documentation/v3/mediasoup/api/#router-close + router[callId].close() + delete router[callId] + producer.close() + consumer.close() + }) + + // from the consumer extract the following params + // to send back to the Client + const params = { + id: consumer.id, + producerId: producer.id, + kind: consumer.kind, + rtpParameters: consumer.rtpParameters, + } + + // send the parameters to the client + callback({ params }) + } + } catch (error) { + console.log(error.message) + callback({ + params: { + error: error + } + }) + } + }) + + socket.on('consumer-resume', async () => { + console.log(`[consumer-resume]`) + await consumer.resume() + }) +}) + +const createWebRtcTransportLayer = async (callId, callback) => { + try { + console.log('[createWebRtcTransportLayer] callId', callId); + // https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions + const webRtcTransport_options = { + listenIps: [ + { + ip: process.env.IP, // Listening IPv4 or IPv6. + announcedIp: process.env.ANNOUNCED_IP, // Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP). + } + ], + enableUdp: true, + enableTcp: true, + preferUdp: true, + } + + // console.log('webRtcTransport_options', webRtcTransport_options); + // console.log('router', router, '| router[callId]', router[callId]); + + // https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport + let transport = await router[callId].createWebRtcTransport(webRtcTransport_options) + console.log(`callId: ${callId} | transport id: ${transport.id}`) + + transport.on('dtlsstatechange', dtlsState => { + if (dtlsState === 'closed') { + transport.close() + } + }) + + transport.on('close', () => { + console.log('transport closed') + }) + + const params = { + id: transport.id, + iceParameters: transport.iceParameters, + iceCandidates: transport.iceCandidates, + dtlsParameters: transport.dtlsParameters, + } + + // console.log('params', params); + + // send back to the client the following prameters + callback({ + // https://mediasoup.org/documentation/v3/mediasoup-client/api/#TransportOptions + params + }) + + return transport + + } catch (error) { + console.log('[createWebRtcTransportLayer] ERROR', JSON.stringify(error)); + callback({ + params: { + error: error + } + }) + } } \ No newline at end of file