7 changed files with 317 additions and 2 deletions
@ -0,0 +1,53 @@ |
|||||
|
// media-router.js
|
||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
|
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
const STATUS_LIST = [ |
||||
|
'starting', // the router process is starting and configuring itself
|
||||
|
'active', // the router is active and available for service
|
||||
|
'capacity', // the router is at or over capacity
|
||||
|
'closing', // the router is closing/shutting down
|
||||
|
'closed', // the router no longer exists
|
||||
|
]; |
||||
|
|
||||
|
const RouterHostSchema = new Schema({ |
||||
|
address: { type: String, required: true, index: 1 }, |
||||
|
port: { type: Number, required: true }, |
||||
|
}); |
||||
|
|
||||
|
/* |
||||
|
* A Router is a "multi-user conference call instance" somewhere on the |
||||
|
* infrastructure. This model helps us manage them, balance load across them, |
||||
|
* and route calls to and between them (for scale). |
||||
|
* |
||||
|
* These records are created when a call is being created, and are commonly |
||||
|
* left in the database after all call participants have left. An expires index |
||||
|
* is used to sweep up router records after 30 days. This allows us to perform |
||||
|
* statistics aggregation on router use and store aggregated results as part of |
||||
|
* long-term reporting. |
||||
|
*/ |
||||
|
const MediaRouterSchema = new Schema({ |
||||
|
created: { type: Date, default: Date.now, required: true, expires: '30d' }, |
||||
|
lastActivity: { type: Date, default: Date.now, required: true }, |
||||
|
status: { type: String, enum: STATUS_LIST, default: 'starting', required: true, index: true }, |
||||
|
name: { type: String }, |
||||
|
description: { type: String }, |
||||
|
access: { |
||||
|
isPrivate: { type: Boolean, default: true, required: true }, |
||||
|
passcodeHash: { type: String, select: false }, |
||||
|
}, |
||||
|
host: { type: RouterHostSchema, required: true, select: false }, |
||||
|
stats: { |
||||
|
routerCount: { type: Number, default: 0, required: true }, |
||||
|
consumerCount: { type: Number, default: 0, required: true }, |
||||
|
producerCount: { type: Number, default: 0, required: true }, |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
module.exports = mongoose.model('MediaRouter', MediaRouterSchema); |
@ -0,0 +1,43 @@ |
|||||
|
// media-worker.js
|
||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
|
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
const STATUS_LIST = [ |
||||
|
'starting', // the router process is starting and configuring itself
|
||||
|
'active', // the router is active and available for service
|
||||
|
'capacity', // the router is at or over capacity
|
||||
|
'closing', // the router is closing/shutting down
|
||||
|
'closed', // the router no longer exists
|
||||
|
]; |
||||
|
|
||||
|
const WebRtcListenSchema = new Schema({ |
||||
|
protocol: { type: String, enum: ['tcp','udp'], required: true }, |
||||
|
ip: { type: String, required: true }, |
||||
|
port: { type: Number, required: true }, |
||||
|
}); |
||||
|
|
||||
|
/* |
||||
|
* A media worker is a host process with one or more MediaRouter instances |
||||
|
* processing multi-user conference calls. |
||||
|
*/ |
||||
|
const MediaWorkerSchema = new Schema({ |
||||
|
created: { type: Date, default: Date.now, required: true, expires: '30d' }, |
||||
|
lastActivity: { type: Date, default: Date.now, required: true }, |
||||
|
status: { type: String, enum: STATUS_LIST, default: 'starting', required: true, index: true }, |
||||
|
webRtcServer: { |
||||
|
listenInfos: { type: [WebRtcListenSchema] }, |
||||
|
}, |
||||
|
stats: { |
||||
|
routerCount: { type: Number, default: 0, required: true }, |
||||
|
consumerCount: { type: Number, default: 0, required: true }, |
||||
|
producerCount: { type: Number, default: 0, required: true }, |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
module.exports = mongoose.model('MediaWorker', MediaWorkerSchema); |
@ -0,0 +1,178 @@ |
|||||
|
// dtp-media-engine.js
|
||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||
|
// All Rights Reserved
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
require('dotenv').config(); |
||||
|
|
||||
|
const path = require('path'); |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const mediasoup = require('mediasoup'); |
||||
|
|
||||
|
const { SiteAsync, SiteCommon, SitePlatform, SiteLog } = require(path.join(__dirname, 'lib', 'site-lib')); |
||||
|
|
||||
|
module.rootPath = __dirname; |
||||
|
module.pkg = require(path.join(module.rootPath, 'package.json')); |
||||
|
module.config = { |
||||
|
component: { name: 'dtpMediaEngine', slug: 'dtp-media-engine' }, |
||||
|
root: module.rootPath, |
||||
|
site: require(path.join(module.rootPath, 'config', 'site')), |
||||
|
webRtcServer: [ |
||||
|
{ |
||||
|
protocol: 'udp', |
||||
|
ip: process.env.MEDIASOUP_WEBRTC_BIND_ADDR || '127.0.0.1', |
||||
|
port: process.env.MEDIASOUP_WEBRTC_BIND_PORT || 20000, |
||||
|
} |
||||
|
] |
||||
|
}; |
||||
|
|
||||
|
module.log = new SiteLog(module, module.config.component); |
||||
|
|
||||
|
class MediaEngineWorker extends SiteCommon { |
||||
|
|
||||
|
constructor ( ) { |
||||
|
super(module, { name: 'dtpMediaWorker', slug: 'dtp-media-worker' }); |
||||
|
this._id = mongoose.Types.ObjectId(); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
await super.start(); |
||||
|
|
||||
|
try { |
||||
|
this.worker = await mediasoup.createWorker({ |
||||
|
logLevel: 'warn', |
||||
|
dtlsCertificateFile: process.env.HTTPS_SSL_CRT, |
||||
|
dtlsPrivateKeyFile: process.env.HTTPS_SSL_KEY, |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
throw new Error(`failed to start mediasoup worker process: ${error.message}`); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const BIND_PORT = 20000 + module.nextWorkerIdx++; |
||||
|
this.webRtcServer = await this.worker.createWebRtcServer({ |
||||
|
listenInfos: [ |
||||
|
{ |
||||
|
protocol: 'udp', |
||||
|
ip: '127.0.0.1', |
||||
|
port: BIND_PORT, |
||||
|
}, |
||||
|
{ |
||||
|
protocol: 'tcp', |
||||
|
ip: '127.0.0.1', |
||||
|
port: BIND_PORT, |
||||
|
}, |
||||
|
], |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
throw new Error(`failed to start mediasoup WebRTC Server: ${error.message}`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async stop ( ) { |
||||
|
if (this.webRtcServer && !this.webRtcServer.closed) { |
||||
|
this.log.info('closing mediasoup WebRTC server'); |
||||
|
this.webRtcServer.close(); |
||||
|
delete this.webRtcServer; |
||||
|
} |
||||
|
|
||||
|
if (this.worker && !this.worker.closed) { |
||||
|
this.log.info('closing mediasoup worker process'); |
||||
|
this.worker.close(); |
||||
|
delete this.worker; |
||||
|
} |
||||
|
|
||||
|
await super.stop(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.onNewWorker = async (worker) => { |
||||
|
module.log.info('new worker created', { worker: worker.pid }); |
||||
|
worker.observer.on('close', ( ) => { |
||||
|
module.log.info('worker shutting down', { worker: worker.pid }); |
||||
|
}); |
||||
|
|
||||
|
worker.observer.on('newrouter', (router) => { |
||||
|
module.log.info('new router created', { worker: worker.pid, router: router.id }); |
||||
|
router.observer.on('close', ( ) => { |
||||
|
module.log.info('router shutting down', { worker: worker.pid, router: router.id }); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
module.createWorker = async ( ) => { |
||||
|
const worker = new MediaEngineWorker(); |
||||
|
module.workers.push(worker); |
||||
|
await worker.start(); |
||||
|
}; |
||||
|
|
||||
|
module.shutdown = async ( ) => { |
||||
|
await SiteAsync.each(module.workers, async (worker) => { |
||||
|
try { |
||||
|
await worker.stop(); |
||||
|
} catch (error) { |
||||
|
module.log.error('failed to stop worker', { error }); |
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
* SERVER PROCESS INIT |
||||
|
*/ |
||||
|
|
||||
|
(async ( ) => { |
||||
|
|
||||
|
process.on('unhandledRejection', (error, p) => { |
||||
|
module.log.error('Unhandled rejection', { |
||||
|
error: error, |
||||
|
promise: p, |
||||
|
stack: error.stack |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
process.on('warning', (error) => { |
||||
|
module.log.alert('warning', { error }); |
||||
|
}); |
||||
|
|
||||
|
process.once('SIGINT', async ( ) => { |
||||
|
module.log.info('SIGINT received'); |
||||
|
module.log.info('requesting shutdown...'); |
||||
|
await module.shutdown(); |
||||
|
const exitCode = await SitePlatform.shutdown(); |
||||
|
process.nextTick(( ) => { |
||||
|
process.exit(exitCode); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
process.once('SIGUSR2', async ( ) => { |
||||
|
await SitePlatform.shutdown(); |
||||
|
process.kill(process.pid, 'SIGUSR2'); |
||||
|
}); |
||||
|
|
||||
|
try { |
||||
|
await SitePlatform.startPlatform(module); |
||||
|
} catch (error) { |
||||
|
module.log.error(`failed to start DTP ${module.config.component.slug} process`, { error }); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
module.log.info('registering mediasoup observer callbacks'); |
||||
|
mediasoup.observer.on('newworker', module.onNewWorker); |
||||
|
|
||||
|
module.log.info('creating mediasoup worker instance'); |
||||
|
|
||||
|
module.nextWorkerIdx = 0; |
||||
|
module.workers = [ ]; |
||||
|
|
||||
|
await module.createWorker(); |
||||
|
|
||||
|
module.log.info('DTP Media Engine online'); |
||||
|
} catch (error) { |
||||
|
module.log.error('failed to start DTP Media Engine', { error }); |
||||
|
process.exit(-1); |
||||
|
} |
||||
|
|
||||
|
})(); |
@ -1063,6 +1063,11 @@ |
|||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.11.tgz#6ea7342dfb379ea1210835bada87b3c512120234" |
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.11.tgz#6ea7342dfb379ea1210835bada87b3c512120234" |
||||
integrity sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw== |
integrity sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw== |
||||
|
|
||||
|
"@types/node@^16.11.10": |
||||
|
version "16.18.3" |
||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.3.tgz#d7f7ba828ad9e540270f01ce00d391c54e6e0abc" |
||||
|
integrity sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg== |
||||
|
|
||||
"@types/[email protected]": |
"@types/[email protected]": |
||||
version "1.17.1" |
version "1.17.1" |
||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" |
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" |
||||
@ -4262,6 +4267,13 @@ gulplog@^1.0.0: |
|||||
dependencies: |
dependencies: |
||||
glogg "^1.0.0" |
glogg "^1.0.0" |
||||
|
|
||||
|
h264-profile-level-id@^1.0.1: |
||||
|
version "1.0.1" |
||||
|
resolved "https://registry.yarnpkg.com/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz#92033c190766c846e57c6a97e4c1d922943a9cce" |
||||
|
integrity sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q== |
||||
|
dependencies: |
||||
|
debug "^4.1.1" |
||||
|
|
||||
has-ansi@^2.0.0: |
has-ansi@^2.0.0: |
||||
version "2.0.0" |
version "2.0.0" |
||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" |
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" |
||||
@ -5591,6 +5603,17 @@ [email protected]: |
|||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" |
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" |
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= |
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= |
||||
|
|
||||
|
mediasoup@3: |
||||
|
version "3.10.12" |
||||
|
resolved "https://registry.yarnpkg.com/mediasoup/-/mediasoup-3.10.12.tgz#509c8c8ebe950dbb056ed8dbd077b3a5e902a229" |
||||
|
integrity sha512-cb+Jn51QQOUrZONT1vUzoIIY0wVGsYVNm8ghOGLcmpM90IVceAhBtqXsT/zUgSSMIS/ZUkUOo8YnyTBZMeqkJg== |
||||
|
dependencies: |
||||
|
"@types/node" "^16.11.10" |
||||
|
debug "^4.3.4" |
||||
|
h264-profile-level-id "^1.0.1" |
||||
|
supports-color "^9.2.3" |
||||
|
uuid "^9.0.0" |
||||
|
|
||||
memory-fs@^0.5.0: |
memory-fs@^0.5.0: |
||||
version "0.5.0" |
version "0.5.0" |
||||
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" |
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" |
||||
@ -8127,6 +8150,11 @@ supports-color@^8.0.0, supports-color@^8.1.1: |
|||||
dependencies: |
dependencies: |
||||
has-flag "^4.0.0" |
has-flag "^4.0.0" |
||||
|
|
||||
|
supports-color@^9.2.3: |
||||
|
version "9.2.3" |
||||
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.2.3.tgz#a6e2c97fc20c80abecd69e50aebe4783ff77d45a" |
||||
|
integrity sha512-aszYUX/DVK/ed5rFLb/dDinVJrQjG/vmU433wtqVSD800rYsJNWxh2R3USV90aLSU+UsyQkbNeffVLzc6B6foA== |
||||
|
|
||||
sver-compat@^1.5.0: |
sver-compat@^1.5.0: |
||||
version "1.5.0" |
version "1.5.0" |
||||
resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" |
resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" |
||||
@ -8711,6 +8739,11 @@ uuid@^8.3.0, uuid@^8.3.2: |
|||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" |
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" |
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== |
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== |
||||
|
|
||||
|
uuid@^9.0.0: |
||||
|
version "9.0.0" |
||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" |
||||
|
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== |
||||
|
|
||||
v8flags@^3.2.0: |
v8flags@^3.2.0: |
||||
version "3.2.0" |
version "3.2.0" |
||||
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" |
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" |
||||
|
Loading…
Reference in new issue