UNPKG

mediasoup

Version:

Cutting Edge WebRTC Video Conferencing

430 lines (429 loc) 16.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Worker = exports.workerBin = void 0; exports.parseWorkerDumpResponse = parseWorkerDumpResponse; 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 Router_1 = require("./Router"); const WebRtcServer_1 = require("./WebRtcServer"); const Transport_1 = require("./Transport"); const utils = require("./utils"); const notification_1 = 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"); // If env MEDIASOUP_WORKER_BIN is given, use it as worker binary. // Otherwise if env MEDIASOUP_BUILDTYPE is 'Debug' use the Debug binary. // Otherwise use the Release binary. exports.workerBin = process.env.MEDIASOUP_WORKER_BIN ? process.env.MEDIASOUP_WORKER_BIN : process.env.MEDIASOUP_BUILDTYPE === 'Debug' ? path.join(__dirname, '..', '..', 'worker', 'out', 'Debug', 'mediasoup-worker') : path.join(__dirname, '..', '..', 'worker', 'out', 'Release', 'mediasoup-worker'); const logger = new Logger_1.Logger('Worker'); const workerLogger = new Logger_1.Logger('Worker'); class Worker 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 subprocess 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(); /** * @private */ constructor({ logLevel, logTags, rtcMinPort, rtcMaxPort, dtlsCertificateFile, dtlsPrivateKeyFile, libwebrtcFieldTrials, appData, }) { super(); logger.debug('constructor()'); let spawnBin = exports.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(exports.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}`); } logger.debug('spawning worker process: %s %s', 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:%s]', this.#pid); this.emit('@success'); } }); this.#child.on('exit', (code, signal) => { // If killed by ourselves, do nothing. if (this.#child.killed) { return; } if (!spawnDone) { spawnDone = true; if (code === 42) { logger.error('worker process failed due to wrong settings [pid:%s]', this.#pid); this.close(); this.emit('@failure', new TypeError('wrong settings')); } else { logger.error('worker process failed unexpectedly [pid:%s, code:%s, signal:%s]', this.#pid, code, signal); this.close(); this.emit('@failure', new Error(`[pid:${this.#pid}, code:${code}, signal:${signal}]`)); } } else { logger.error('worker process died unexpectedly [pid:%s, code:%s, signal:%s]', this.#pid, code, signal); this.workerDied(new Error(`[pid:${this.#pid}, code:${code}, signal:${signal}]`)); } }); this.#child.on('error', error => { // If killed by ourselves, do nothing. if (this.#child.killed) { return; } if (!spawnDone) { spawnDone = true; logger.error('worker process failed [pid:%s]: %s', this.#pid, error.message); this.close(); this.emit('@failure', error); } else { logger.error('worker process error [pid:%s]: %s', this.#pid, error.message); this.workerDied(error); } }); this.#child.on('close', (code, signal) => { logger.debug('worker subprocess closed [pid:%s, code:%s, signal:%s]', this.#pid, code, signal); this.#subprocessClosed = true; 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}`); } } }); } /** * Worker process identifier (PID). */ get pid() { return this.#pid; } /** * Whether the Worker is closed. */ get closed() { return this.#closed; } /** * Whether the Worker died. */ get died() { return this.#died; } /** * Whether the Worker subprocess is closed. */ get subprocessClosed() { return this.#subprocessClosed; } /** * App custom data. */ get appData() { return this.#appData; } /** * App custom data setter. */ set appData(appData) { this.#appData = appData; } /** * Observer. */ get observer() { return this.#observer; } /** * @private * Just for testing purposes. */ get webRtcServersForTesting() { return this.#webRtcServers; } /** * @private * Just for testing purposes. */ get routersForTesting() { return this.#routers; } /** * Close the Worker. */ close() { if (this.#closed) { return; } logger.debug('close()'); this.#closed = true; // Kill the worker process. this.#child.kill('SIGTERM'); // 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(); // Emit observer event. this.#observer.safeEmit('close'); } /** * Dump Worker. */ 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); } /** * Get mediasoup-worker process resource usage. */ 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(); /* eslint-disable camelcase */ 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), }; /* eslint-enable camelcase */ } /** * Update settings. */ 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); } /** * Create a WebRtcServer. */ 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, 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.WebRtcServer({ 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; } /** * Create a Router. */ 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.Router({ 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(`died() [error:${error}]`); this.#closed = 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(); this.safeEmit('died', error); // Emit observer event. this.#observer.safeEmit('close'); } } exports.Worker = Worker; function parseWorkerDumpResponse(binary) { const dump = { pid: binary.pid(), webRtcServerIds: utils.parseVector(binary, 'webRtcServerIds'), routerIds: utils.parseVector(binary, 'routerIds'), channelMessageHandlers: { channelRequestHandlers: utils.parseVector(binary.channelMessageHandlers(), 'channelRequestHandlers'), channelNotificationHandlers: utils.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; }