UNPKG

mediasoup

Version:

Cutting Edge WebRTC Video Conferencing

422 lines (421 loc) 17.6 kB
"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; }