UNPKG

mediasoup

Version:

Cutting Edge WebRTC Video Conferencing

352 lines (351 loc) 12.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProducerImpl = void 0; exports.producerTypeFromFbs = producerTypeFromFbs; exports.producerTypeToFbs = producerTypeToFbs; const Logger_1 = require("./Logger"); const enhancedEvents_1 = require("./enhancedEvents"); const rtpParametersFbsUtils_1 = require("./rtpParametersFbsUtils"); const rtpStreamStatsFbsUtils_1 = require("./rtpStreamStatsFbsUtils"); const fbsUtils = require("./fbsUtils"); const notification_1 = require("./fbs/notification"); const common_1 = require("./fbs/common"); const FbsNotification = require("./fbs/notification"); const FbsRequest = require("./fbs/request"); const FbsTransport = require("./fbs/transport"); const FbsProducer = require("./fbs/producer"); const FbsProducerTraceInfo = require("./fbs/producer/trace-info"); const FbsRtpParameters = require("./fbs/rtp-parameters"); const logger = new Logger_1.Logger('Producer'); class ProducerImpl extends enhancedEvents_1.EnhancedEventEmitter { // Internal data. #internal; // Producer data. #data; // Channel instance. #channel; // Closed flag. #closed = false; // Paused flag. #paused = false; // Custom app data. #appData; // Current score. #score = []; // Observer instance. #observer = new enhancedEvents_1.EnhancedEventEmitter(); constructor({ internal, data, channel, appData, paused, }) { super(); logger.debug('constructor()'); this.#internal = internal; this.#data = data; this.#channel = channel; this.#paused = paused; this.#appData = appData ?? {}; this.handleWorkerNotifications(); this.handleListenerError(); } get id() { return this.#internal.producerId; } get closed() { return this.#closed; } get kind() { return this.#data.kind; } get rtpParameters() { return this.#data.rtpParameters; } get type() { return this.#data.type; } get consumableRtpParameters() { return this.#data.consumableRtpParameters; } get paused() { return this.#paused; } get score() { return this.#score; } get appData() { return this.#appData; } set appData(appData) { this.#appData = appData; } get observer() { return this.#observer; } /** * Just for testing purposes. * * @private */ get channelForTesting() { return this.#channel; } close() { if (this.#closed) { return; } logger.debug('close()'); this.#closed = true; // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.producerId); /* Build Request. */ const requestOffset = new FbsTransport.CloseProducerRequestT(this.#internal.producerId).pack(this.#channel.bufferBuilder); this.#channel .request(FbsRequest.Method.TRANSPORT_CLOSE_PRODUCER, FbsRequest.Body.Transport_CloseProducerRequest, requestOffset, this.#internal.transportId) .catch(() => { }); this.emit('@close'); // Emit observer event. this.#observer.safeEmit('close'); } transportClosed() { if (this.#closed) { return; } logger.debug('transportClosed()'); this.#closed = true; // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.producerId); this.safeEmit('transportclose'); // Emit observer event. this.#observer.safeEmit('close'); } async dump() { logger.debug('dump()'); const response = await this.#channel.request(FbsRequest.Method.PRODUCER_DUMP, undefined, undefined, this.#internal.producerId); /* Decode Response. */ const dumpResponse = new FbsProducer.DumpResponse(); response.body(dumpResponse); return parseProducerDump(dumpResponse); } async getStats() { logger.debug('getStats()'); const response = await this.#channel.request(FbsRequest.Method.PRODUCER_GET_STATS, undefined, undefined, this.#internal.producerId); /* Decode Response. */ const data = new FbsProducer.GetStatsResponse(); response.body(data); return parseProducerStats(data); } async pause() { logger.debug('pause()'); await this.#channel.request(FbsRequest.Method.PRODUCER_PAUSE, undefined, undefined, this.#internal.producerId); const wasPaused = this.#paused; this.#paused = true; // Emit observer event. if (!wasPaused) { this.#observer.safeEmit('pause'); } } async resume() { logger.debug('resume()'); await this.#channel.request(FbsRequest.Method.PRODUCER_RESUME, undefined, undefined, this.#internal.producerId); const wasPaused = this.#paused; this.#paused = false; // Emit observer event. if (wasPaused) { this.#observer.safeEmit('resume'); } } async enableTraceEvent(types = []) { logger.debug('enableTraceEvent()'); if (!Array.isArray(types)) { throw new TypeError('types must be an array'); } if (types.find(type => typeof type !== 'string')) { throw new TypeError('every type must be a string'); } // Convert event types. const fbsEventTypes = []; for (const eventType of types) { try { fbsEventTypes.push(producerTraceEventTypeToFbs(eventType)); } catch (error) { logger.warn('enableTraceEvent() | [error:${error}]'); } } /* Build Request. */ const requestOffset = new FbsProducer.EnableTraceEventRequestT(fbsEventTypes).pack(this.#channel.bufferBuilder); await this.#channel.request(FbsRequest.Method.PRODUCER_ENABLE_TRACE_EVENT, FbsRequest.Body.Producer_EnableTraceEventRequest, requestOffset, this.#internal.producerId); } send(rtpPacket) { if (!Buffer.isBuffer(rtpPacket)) { throw new TypeError('rtpPacket must be a Buffer'); } const builder = this.#channel.bufferBuilder; const dataOffset = FbsProducer.SendNotification.createDataVector(builder, rtpPacket); const notificationOffset = FbsProducer.SendNotification.createSendNotification(builder, dataOffset); this.#channel.notify(FbsNotification.Event.PRODUCER_SEND, FbsNotification.Body.Producer_SendNotification, notificationOffset, this.#internal.producerId); } handleWorkerNotifications() { this.#channel.on(this.#internal.producerId, (event, data) => { switch (event) { case notification_1.Event.PRODUCER_SCORE: { const notification = new FbsProducer.ScoreNotification(); data.body(notification); const score = fbsUtils.parseVector(notification, 'scores', parseProducerScore); this.#score = score; this.safeEmit('score', score); // Emit observer event. this.#observer.safeEmit('score', score); break; } case notification_1.Event.PRODUCER_VIDEO_ORIENTATION_CHANGE: { const notification = new FbsProducer.VideoOrientationChangeNotification(); data.body(notification); const videoOrientation = notification.unpack(); this.safeEmit('videoorientationchange', videoOrientation); // Emit observer event. this.#observer.safeEmit('videoorientationchange', videoOrientation); break; } case notification_1.Event.PRODUCER_TRACE: { const notification = new FbsProducer.TraceNotification(); data.body(notification); const trace = parseTraceEventData(notification); this.safeEmit('trace', trace); // Emit observer event. this.#observer.safeEmit('trace', trace); break; } default: { logger.error(`ignoring unknown event "${event}"`); } } }); } handleListenerError() { this.on('listenererror', (eventName, error) => { logger.error(`event listener threw an error [eventName:${eventName}]:`, error); }); } } exports.ProducerImpl = ProducerImpl; function producerTypeFromFbs(type) { switch (type) { case FbsRtpParameters.Type.SIMPLE: { return 'simple'; } case FbsRtpParameters.Type.SIMULCAST: { return 'simulcast'; } case FbsRtpParameters.Type.SVC: { return 'svc'; } default: { throw new TypeError(`invalid FbsRtpParameters.Type: ${type}`); } } } function producerTypeToFbs(type) { switch (type) { case 'simple': { return FbsRtpParameters.Type.SIMPLE; } case 'simulcast': { return FbsRtpParameters.Type.SIMULCAST; } case 'svc': { return FbsRtpParameters.Type.SVC; } default: { throw new TypeError(`invalid ProducerType: ${type}`); } } } function producerTraceEventTypeToFbs(eventType) { switch (eventType) { case 'keyframe': { return FbsProducer.TraceEventType.KEYFRAME; } case 'fir': { return FbsProducer.TraceEventType.FIR; } case 'nack': { return FbsProducer.TraceEventType.NACK; } case 'pli': { return FbsProducer.TraceEventType.PLI; } case 'rtp': { return FbsProducer.TraceEventType.RTP; } case 'sr': { return FbsProducer.TraceEventType.SR; } default: { throw new TypeError(`invalid ProducerTraceEventType: ${eventType}`); } } } function producerTraceEventTypeFromFbs(eventType) { switch (eventType) { case FbsProducer.TraceEventType.KEYFRAME: { return 'keyframe'; } case FbsProducer.TraceEventType.FIR: { return 'fir'; } case FbsProducer.TraceEventType.NACK: { return 'nack'; } case FbsProducer.TraceEventType.PLI: { return 'pli'; } case FbsProducer.TraceEventType.RTP: { return 'rtp'; } case FbsProducer.TraceEventType.SR: { return 'sr'; } } } function parseProducerDump(data) { return { id: data.id(), kind: data.kind() === FbsRtpParameters.MediaKind.AUDIO ? 'audio' : 'video', type: producerTypeFromFbs(data.type()), rtpParameters: (0, rtpParametersFbsUtils_1.parseRtpParameters)(data.rtpParameters()), // NOTE: optional values are represented with null instead of undefined. // TODO: Make flatbuffers TS return undefined instead of null. rtpMapping: data.rtpMapping()?.unpack(), // NOTE: optional values are represented with null instead of undefined. // TODO: Make flatbuffers TS return undefined instead of null. rtpStreams: data.rtpStreamsLength() > 0 ? fbsUtils.parseVector(data, 'rtpStreams', rtpStream => rtpStream.unpack()) : undefined, traceEventTypes: fbsUtils.parseVector(data, 'traceEventTypes', producerTraceEventTypeFromFbs), paused: data.paused(), }; } function parseProducerStats(binary) { return fbsUtils.parseVector(binary, 'stats', rtpStreamStatsFbsUtils_1.parseRtpStreamRecvStats); } function parseProducerScore(binary) { return { encodingIdx: binary.encodingIdx(), ssrc: binary.ssrc(), rid: binary.rid() ?? undefined, score: binary.score(), }; } function parseTraceEventData(trace) { // eslint-disable-next-line @typescript-eslint/no-explicit-any let info; if (trace.infoType() !== FbsProducer.TraceInfo.NONE) { const accessor = trace.info.bind(trace); info = FbsProducerTraceInfo.unionToTraceInfo(trace.infoType(), accessor); trace.info(info); } return { type: producerTraceEventTypeFromFbs(trace.type()), timestamp: Number(trace.timestamp()), direction: trace.direction() === common_1.TraceDirection.DIRECTION_IN ? 'in' : 'out', info: info?.unpack(), }; }