Browse Source

basic mediasoup integration

pull/2/head
Rob Colbert 1 year ago
parent
commit
d76fa625d0
  1. 53
      app/models/media-router.js
  2. 43
      app/models/media-worker.js
  3. 4
      client/js/index.js
  4. 178
      dtp-media-engine.js
  5. 7
      lib/site-platform.js
  6. 1
      package.json
  7. 33
      yarn.lock

53
app/models/media-router.js

@ -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);

43
app/models/media-worker.js

@ -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);

4
client/js/index.js

@ -28,7 +28,9 @@ window.addEventListener('load', async ( ) => {
// service worker
if ('serviceWorker' in navigator) {
try {
dtp.registration = await navigator.serviceWorker.register('/dist/js/service_worker.min.js');
dtp.registration = await navigator.serviceWorker.register('/dist/js/service_worker.min.js', {
scope: '/',
});
dtp.log.info('load', 'service worker startup complete', { scope: dtp.registration.scope });
} catch (error) {
console.log('service worker startup failed', { error });

178
dtp-media-engine.js

@ -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);
}
})();

7
lib/site-platform.js

@ -250,11 +250,16 @@ module.exports.startWebServer = async (dtp) => {
return next();
}
function serviceWorkerAllowed (req, res, next) {
res.set('Service-Worker-Allowed', '/');
return next();
}
/*
* Static file services (project)
*/
module.app.use(express.static(path.join(dtp.config.root, 'client')));
module.app.use('/dist', cacheOneDay, express.static(path.join(dtp.config.root, 'dist')));
module.app.use('/dist', cacheOneDay, serviceWorkerAllowed, express.static(path.join(dtp.config.root, 'dist')));
module.app.use('/img', cacheOneDay, express.static(path.join(dtp.config.root, 'client', 'img')));
/*

1
package.json

@ -46,6 +46,7 @@
"jsdom": "^19.0.0",
"libphonenumber-js": "^1.9.49",
"marked": "^4.0.12",
"mediasoup": "3",
"method-override": "^3.0.0",
"mime": "^3.0.0",
"minio": "^7.0.26",

33
yarn.lock

@ -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…
Cancel
Save