mediasoup
Version:
Cutting Edge WebRTC Video Conferencing
422 lines (421 loc) • 17.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkerImpl = exports.defaultWorkerBin = void 0;
const process = require("node:process");
const path = require("node:path");
const node_child_process_1 = require("node:child_process");
const _1 = require("./");
const Logger_1 = require("./Logger");
const enhancedEvents_1 = require("./enhancedEvents");
const ortc = require("./ortc");
const Channel_1 = require("./Channel");
const WebRtcServer_1 = require("./WebRtcServer");
const Router_1 = require("./Router");
const Transport_1 = require("./Transport");
const utils = require("./utils");
const fbsUtils = require("./fbsUtils");
const notification_1 = require("./fbs/notification");
const FbsNotification = require("./fbs/notification");
const FbsRequest = require("./fbs/request");
const FbsWorker = require("./fbs/worker");
const FbsTransport = require("./fbs/transport");
const protocol_1 = require("./fbs/transport/protocol");
const logger = new Logger_1.Logger('Worker');
const workerLogger = new Logger_1.Logger('Worker');
exports.defaultWorkerBin = getDefaultWorkerBin();
class WorkerImpl extends enhancedEvents_1.EnhancedEventEmitter {
// mediasoup-worker child process.
#child;
// Worker process PID.
#pid;
// Channel instance.
#channel;
// Closed flag.
#closed = false;
// Died dlag.
#died = false;
// Worker process closed flag.
#subprocessClosed = false;
// Custom app data.
#appData;
// WebRtcServers set.
#webRtcServers = new Set();
// Routers set.
#routers = new Set();
// Observer instance.
#observer = new enhancedEvents_1.EnhancedEventEmitter();
constructor({ logLevel, logTags, rtcMinPort, rtcMaxPort, dtlsCertificateFile, dtlsPrivateKeyFile, workerBin, libwebrtcFieldTrials, disableLiburing, appData, }) {
super();
logger.debug('constructor()');
workerBin = workerBin ?? exports.defaultWorkerBin;
let spawnBin = workerBin;
let spawnArgs = [];
if (process.env['MEDIASOUP_USE_VALGRIND'] === 'true') {
spawnBin = process.env['MEDIASOUP_VALGRIND_BIN'] ?? 'valgrind';
if (process.env['MEDIASOUP_VALGRIND_OPTIONS']) {
spawnArgs = spawnArgs.concat(process.env['MEDIASOUP_VALGRIND_OPTIONS'].split(/\s+/));
}
spawnArgs.push(workerBin);
}
if (typeof logLevel === 'string' && logLevel) {
spawnArgs.push(`--logLevel=${logLevel}`);
}
for (const logTag of Array.isArray(logTags) ? logTags : []) {
if (typeof logTag === 'string' && logTag) {
spawnArgs.push(`--logTag=${logTag}`);
}
}
if (typeof rtcMinPort === 'number' && !Number.isNaN(rtcMinPort)) {
spawnArgs.push(`--rtcMinPort=${rtcMinPort}`);
}
if (typeof rtcMaxPort === 'number' && !Number.isNaN(rtcMaxPort)) {
spawnArgs.push(`--rtcMaxPort=${rtcMaxPort}`);
}
if (typeof dtlsCertificateFile === 'string' && dtlsCertificateFile) {
spawnArgs.push(`--dtlsCertificateFile=${dtlsCertificateFile}`);
}
if (typeof dtlsPrivateKeyFile === 'string' && dtlsPrivateKeyFile) {
spawnArgs.push(`--dtlsPrivateKeyFile=${dtlsPrivateKeyFile}`);
}
if (typeof libwebrtcFieldTrials === 'string' && libwebrtcFieldTrials) {
spawnArgs.push(`--libwebrtcFieldTrials=${libwebrtcFieldTrials}`);
}
if (disableLiburing) {
spawnArgs.push(`--disableLiburing=true`);
}
logger.debug(`spawning worker process: ${spawnBin} ${spawnArgs.join(' ')}`);
this.#child = (0, node_child_process_1.spawn)(
// command
spawnBin,
// args
spawnArgs,
// options
{
env: {
MEDIASOUP_VERSION: _1.version,
// Let the worker process inherit all environment variables, useful
// if a custom and not in the path GCC is used so the user can set
// LD_LIBRARY_PATH environment variable for runtime.
...process.env,
},
detached: false,
// fd 0 (stdin) : Just ignore it.
// fd 1 (stdout) : Pipe it for 3rd libraries that log their own stuff.
// fd 2 (stderr) : Same as stdout.
// fd 3 (channel) : Producer Channel fd.
// fd 4 (channel) : Consumer Channel fd.
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
windowsHide: true,
});
this.#pid = this.#child.pid;
this.#channel = new Channel_1.Channel({
producerSocket: this.#child.stdio[3],
consumerSocket: this.#child.stdio[4],
pid: this.#pid,
});
this.#appData = appData ?? {};
let spawnDone = false;
// Listen for 'running' notification.
this.#channel.once(String(this.#pid), (event) => {
if (!spawnDone && event === notification_1.Event.WORKER_RUNNING) {
spawnDone = true;
logger.debug(`worker process running [pid:${this.#pid}]`);
this.emit('@success');
}
});
this.#child.on('exit', (code, signal) => {
// If closed by ourselves, do nothing.
if (this.#closed) {
return;
}
if (!spawnDone) {
spawnDone = true;
if (code === 42) {
logger.error(`worker process failed due to wrong settings [pid:${this.#pid}]`);
this.close();
this.emit('@failure', new TypeError('wrong settings'));
}
else {
logger.error(`worker process failed unexpectedly [pid:${this.#pid}, code:${code}, signal:${signal}]`);
this.close();
this.emit('@failure', new Error(`[pid:${this.#pid}, code:${code}, signal:${signal}]`));
}
}
else {
logger.error(`worker process died unexpectedly [pid:${this.#pid}, code:${code}, signal:${signal}]`);
this.workerDied(new Error(`[pid:${this.#pid}, code:${code}, signal:${signal}]`));
}
});
this.#child.on('error', error => {
// If closed by ourselves, do nothing.
if (this.#closed) {
return;
}
if (!spawnDone) {
spawnDone = true;
logger.error(`worker process failed [pid:${this.#pid}]: ${error.message}`);
this.close();
this.emit('@failure', new Error(error.message));
}
else {
logger.error(`worker process error [pid:${this.#pid}]: ${error.message}`);
this.workerDied(error);
}
});
this.#child.on('close', (code, signal) => {
logger.debug(`worker process closed [pid:${this.#pid}, code:${code}, signal:${signal}]`);
if (!this.#subprocessClosed) {
this.#subprocessClosed = true;
logger.debug(`emitting 'subprocessclose' event`);
this.safeEmit('subprocessclose');
}
});
// Be ready for 3rd party worker libraries logging to stdout.
this.#child.stdout.on('data', buffer => {
for (const line of buffer.toString('utf8').split('\n')) {
if (line) {
workerLogger.debug(`(stdout) ${line}`);
}
}
});
// In case of a worker bug, mediasoup will log to stderr.
this.#child.stderr.on('data', buffer => {
for (const line of buffer.toString('utf8').split('\n')) {
if (line) {
workerLogger.error(`(stderr) ${line}`);
}
}
});
this.handleListenerError();
}
get pid() {
return this.#pid;
}
get closed() {
return this.#closed;
}
get died() {
return this.#died;
}
get subprocessClosed() {
return this.#subprocessClosed;
}
get appData() {
return this.#appData;
}
set appData(appData) {
this.#appData = appData;
}
get observer() {
return this.#observer;
}
/**
* Just for testing purposes.
*/
get webRtcServersForTesting() {
return this.#webRtcServers;
}
/**
* Just for testing purposes.
*/
get routersForTesting() {
return this.#routers;
}
close() {
if (this.#closed) {
return;
}
logger.debug('close()');
this.#closed = true;
// Close every Router.
for (const router of this.#routers) {
router.workerClosed();
}
this.#routers.clear();
// Close every WebRtcServer.
for (const webRtcServer of this.#webRtcServers) {
webRtcServer.workerClosed();
}
this.#webRtcServers.clear();
// Send notification to worker process.
this.#channel.notify(FbsNotification.Event.WORKER_CLOSE);
// Close the Channel instance now.
this.#channel.close();
// Emit observer event.
this.#observer.safeEmit('close');
}
async dump() {
logger.debug('dump()');
// Send the request and wait for the response.
const response = await this.#channel.request(FbsRequest.Method.WORKER_DUMP);
/* Decode Response. */
const dump = new FbsWorker.DumpResponse();
response.body(dump);
return parseWorkerDumpResponse(dump);
}
async getResourceUsage() {
logger.debug('getResourceUsage()');
const response = await this.#channel.request(FbsRequest.Method.WORKER_GET_RESOURCE_USAGE);
/* Decode Response. */
const resourceUsage = new FbsWorker.ResourceUsageResponse();
response.body(resourceUsage);
const ru = resourceUsage.unpack();
return {
ru_utime: Number(ru.ruUtime),
ru_stime: Number(ru.ruStime),
ru_maxrss: Number(ru.ruMaxrss),
ru_ixrss: Number(ru.ruIxrss),
ru_idrss: Number(ru.ruIdrss),
ru_isrss: Number(ru.ruIsrss),
ru_minflt: Number(ru.ruMinflt),
ru_majflt: Number(ru.ruMajflt),
ru_nswap: Number(ru.ruNswap),
ru_inblock: Number(ru.ruInblock),
ru_oublock: Number(ru.ruOublock),
ru_msgsnd: Number(ru.ruMsgsnd),
ru_msgrcv: Number(ru.ruMsgrcv),
ru_nsignals: Number(ru.ruNsignals),
ru_nvcsw: Number(ru.ruNvcsw),
ru_nivcsw: Number(ru.ruNivcsw),
};
}
async updateSettings({ logLevel, logTags, } = {}) {
logger.debug('updateSettings()');
// Build the request.
const requestOffset = new FbsWorker.UpdateSettingsRequestT(logLevel, logTags).pack(this.#channel.bufferBuilder);
await this.#channel.request(FbsRequest.Method.WORKER_UPDATE_SETTINGS, FbsRequest.Body.Worker_UpdateSettingsRequest, requestOffset);
}
async createWebRtcServer({ listenInfos, appData, }) {
logger.debug('createWebRtcServer()');
if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
// Build the request.
const fbsListenInfos = [];
for (const listenInfo of listenInfos) {
fbsListenInfos.push(new FbsTransport.ListenInfoT(listenInfo.protocol === 'udp'
? protocol_1.Protocol.UDP
: protocol_1.Protocol.TCP, listenInfo.ip, listenInfo.announcedAddress ?? listenInfo.announcedIp, Boolean(listenInfo.exposeInternalIp), listenInfo.port, (0, Transport_1.portRangeToFbs)(listenInfo.portRange), (0, Transport_1.socketFlagsToFbs)(listenInfo.flags), listenInfo.sendBufferSize, listenInfo.recvBufferSize));
}
const webRtcServerId = utils.generateUUIDv4();
const createWebRtcServerRequestOffset = new FbsWorker.CreateWebRtcServerRequestT(webRtcServerId, fbsListenInfos).pack(this.#channel.bufferBuilder);
await this.#channel.request(FbsRequest.Method.WORKER_CREATE_WEBRTCSERVER, FbsRequest.Body.Worker_CreateWebRtcServerRequest, createWebRtcServerRequestOffset);
const webRtcServer = new WebRtcServer_1.WebRtcServerImpl({
internal: { webRtcServerId },
channel: this.#channel,
appData,
});
this.#webRtcServers.add(webRtcServer);
webRtcServer.on('@close', () => this.#webRtcServers.delete(webRtcServer));
// Emit observer event.
this.#observer.safeEmit('newwebrtcserver', webRtcServer);
return webRtcServer;
}
async createRouter({ mediaCodecs, appData, } = {}) {
logger.debug('createRouter()');
if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
// Clone given media codecs to not modify input data.
const clonedMediaCodecs = utils.clone(mediaCodecs);
// This may throw.
const rtpCapabilities = ortc.generateRouterRtpCapabilities(clonedMediaCodecs);
const routerId = utils.generateUUIDv4();
// Get flatbuffer builder.
const createRouterRequestOffset = new FbsWorker.CreateRouterRequestT(routerId).pack(this.#channel.bufferBuilder);
await this.#channel.request(FbsRequest.Method.WORKER_CREATE_ROUTER, FbsRequest.Body.Worker_CreateRouterRequest, createRouterRequestOffset);
const data = { rtpCapabilities };
const router = new Router_1.RouterImpl({
internal: {
routerId,
},
data,
channel: this.#channel,
appData,
});
this.#routers.add(router);
router.on('@close', () => this.#routers.delete(router));
// Emit observer event.
this.#observer.safeEmit('newrouter', router);
return router;
}
workerDied(error) {
if (this.#closed) {
return;
}
logger.debug(`workerDied() [error:${error.toString()}]`);
this.#closed = true;
this.#subprocessClosed = true;
this.#died = true;
// Close the Channel instance.
this.#channel.close();
// Close every Router.
for (const router of this.#routers) {
router.workerClosed();
}
this.#routers.clear();
// Close every WebRtcServer.
for (const webRtcServer of this.#webRtcServers) {
webRtcServer.workerClosed();
}
this.#webRtcServers.clear();
logger.debug(`workerDied() | emitting 'died' and 'subprocessclose' events`);
this.safeEmit('died', error);
this.safeEmit('subprocessclose');
// Emit observer event.
this.#observer.safeEmit('close');
}
handleListenerError() {
this.on('listenererror', (eventName, error) => {
logger.error(`event listener threw an error [eventName:${eventName}]:`, error);
});
}
}
exports.WorkerImpl = WorkerImpl;
function parseWorkerDumpResponse(binary) {
const dump = {
pid: binary.pid(),
webRtcServerIds: fbsUtils.parseVector(binary, 'webRtcServerIds'),
routerIds: fbsUtils.parseVector(binary, 'routerIds'),
channelMessageHandlers: {
channelRequestHandlers: fbsUtils.parseVector(binary.channelMessageHandlers(), 'channelRequestHandlers'),
channelNotificationHandlers: fbsUtils.parseVector(binary.channelMessageHandlers(), 'channelNotificationHandlers'),
},
};
if (binary.liburing()) {
dump.liburing = {
sqeProcessCount: Number(binary.liburing().sqeProcessCount()),
sqeMissCount: Number(binary.liburing().sqeMissCount()),
userDataMissCount: Number(binary.liburing().userDataMissCount()),
};
}
return dump;
}
function getDefaultWorkerBin() {
// If MEDIASOUP_WORKER_BIN env is given, use it as worker binary.
if (process.env['MEDIASOUP_WORKER_BIN']) {
logger.debug(`getDefaultWorkerBin() | using MEDIASOUP_WORKER_BIN environment variable: ${process.env['MEDIASOUP_WORKER_BIN']}`);
return process.env['MEDIASOUP_WORKER_BIN'];
}
// Obtain the path of the mediasoup module.
let mediasoupModulePath;
try {
// NOTE: This will throw `MODULE_NOT_FOUND` if mediasoup is installed
// globally.
mediasoupModulePath = require.resolve('mediasoup');
// NOTE: Returned path will include 'node/lib/index.js' since that's the
// main entry point in package.json, so remove it.
mediasoupModulePath = path.join(path.dirname(mediasoupModulePath), '..', '..');
}
catch (error) {
logger.warn(`getDefaultWorkerBin() | require.resolve('mediasoup') failed, using __dirname: ${error}`);
// mediasoup module path is two folders above this file.
mediasoupModulePath = path.join(__dirname, '..', '..');
}
// If env MEDIASOUP_BUILDTYPE is 'Debug' use the Debug binary. Otherwise use
// the Release binary.
const buildType = process.env['MEDIASOUP_BUILDTYPE'] === 'Debug' ? 'Debug' : 'Release';
const defaultWorkerBinPath = path.join(mediasoupModulePath, 'worker', 'out', buildType, 'mediasoup-worker');
logger.debug(`getDefaultWorkerBin() | detected worker binary path: ${defaultWorkerBinPath}`);
return defaultWorkerBinPath;
}