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" |
|||
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]": |
|||
version "1.17.1" |
|||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" |
|||
@ -4262,6 +4267,13 @@ gulplog@^1.0.0: |
|||
dependencies: |
|||
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: |
|||
version "2.0.0" |
|||
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" |
|||
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: |
|||
version "0.5.0" |
|||
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: |
|||
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: |
|||
version "1.5.0" |
|||
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" |
|||
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: |
|||
version "3.2.0" |
|||
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" |
|||
|
Loading…
Reference in new issue