UNPKG

mediasoup

Version:

Cutting Edge WebRTC Video Conferencing

618 lines (617 loc) 21.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Consumer = void 0; exports.parseTraceEventData = parseTraceEventData; const Logger_1 = require("./Logger"); const enhancedEvents_1 = require("./enhancedEvents"); const RtpParameters_1 = require("./RtpParameters"); const RtpStream_1 = require("./RtpStream"); const utils = require("./utils"); const notification_1 = require("./fbs/notification"); const common_1 = require("./fbs/common"); const FbsRequest = require("./fbs/request"); const FbsTransport = require("./fbs/transport"); const FbsConsumer = require("./fbs/consumer"); const FbsConsumerTraceInfo = require("./fbs/consumer/trace-info"); const rtp_parameters_1 = require("./fbs/rtp-parameters"); const FbsRtpParameters = require("./fbs/rtp-parameters"); const logger = new Logger_1.Logger('Consumer'); class Consumer extends enhancedEvents_1.EnhancedEventEmitter { // Internal data. #internal; // Consumer data. #data; // Channel instance. #channel; // Closed flag. #closed = false; // Custom app data. #appData; // Paused flag. #paused = false; // Associated Producer paused flag. #producerPaused = false; // Current priority. #priority = 1; // Current score. #score; // Preferred layers. #preferredLayers; // Curent layers. #currentLayers; // Observer instance. #observer = new enhancedEvents_1.EnhancedEventEmitter(); /** * @private */ constructor({ internal, data, channel, appData, paused, producerPaused, score = { score: 10, producerScore: 10, producerScores: [] }, preferredLayers, }) { super(); logger.debug('constructor()'); this.#internal = internal; this.#data = data; this.#channel = channel; this.#paused = paused; this.#producerPaused = producerPaused; this.#score = score; this.#preferredLayers = preferredLayers; this.#appData = appData || {}; this.handleWorkerNotifications(); } /** * Consumer id. */ get id() { return this.#internal.consumerId; } /** * Associated Producer id. */ get producerId() { return this.#data.producerId; } /** * Whether the Consumer is closed. */ get closed() { return this.#closed; } /** * Media kind. */ get kind() { return this.#data.kind; } /** * RTP parameters. */ get rtpParameters() { return this.#data.rtpParameters; } /** * Consumer type. */ get type() { return this.#data.type; } /** * Whether the Consumer is paused. */ get paused() { return this.#paused; } /** * Whether the associate Producer is paused. */ get producerPaused() { return this.#producerPaused; } /** * Current priority. */ get priority() { return this.#priority; } /** * Consumer score. */ get score() { return this.#score; } /** * Preferred video layers. */ get preferredLayers() { return this.#preferredLayers; } /** * Current video layers. */ get currentLayers() { return this.#currentLayers; } /** * 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 channelForTesting() { return this.#channel; } /** * Close the Consumer. */ close() { if (this.#closed) { return; } logger.debug('close()'); this.#closed = true; // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.consumerId); /* Build Request. */ const requestOffset = new FbsTransport.CloseConsumerRequestT(this.#internal.consumerId).pack(this.#channel.bufferBuilder); this.#channel .request(FbsRequest.Method.TRANSPORT_CLOSE_CONSUMER, FbsRequest.Body.Transport_CloseConsumerRequest, requestOffset, this.#internal.transportId) .catch(() => { }); this.emit('@close'); // Emit observer event. this.#observer.safeEmit('close'); } /** * Transport was closed. * * @private */ transportClosed() { if (this.#closed) { return; } logger.debug('transportClosed()'); this.#closed = true; // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.consumerId); this.safeEmit('transportclose'); // Emit observer event. this.#observer.safeEmit('close'); } /** * Dump Consumer. */ async dump() { logger.debug('dump()'); const response = await this.#channel.request(FbsRequest.Method.CONSUMER_DUMP, undefined, undefined, this.#internal.consumerId); /* Decode Response. */ const data = new FbsConsumer.DumpResponse(); response.body(data); return parseConsumerDumpResponse(data); } /** * Get Consumer stats. */ async getStats() { logger.debug('getStats()'); const response = await this.#channel.request(FbsRequest.Method.CONSUMER_GET_STATS, undefined, undefined, this.#internal.consumerId); /* Decode Response. */ const data = new FbsConsumer.GetStatsResponse(); response.body(data); return parseConsumerStats(data); } /** * Pause the Consumer. */ async pause() { logger.debug('pause()'); await this.#channel.request(FbsRequest.Method.CONSUMER_PAUSE, undefined, undefined, this.#internal.consumerId); const wasPaused = this.#paused; this.#paused = true; // Emit observer event. if (!wasPaused && !this.#producerPaused) { this.#observer.safeEmit('pause'); } } /** * Resume the Consumer. */ async resume() { logger.debug('resume()'); await this.#channel.request(FbsRequest.Method.CONSUMER_RESUME, undefined, undefined, this.#internal.consumerId); const wasPaused = this.#paused; this.#paused = false; // Emit observer event. if (wasPaused && !this.#producerPaused) { this.#observer.safeEmit('resume'); } } /** * Set preferred video layers. */ async setPreferredLayers({ spatialLayer, temporalLayer, }) { logger.debug('setPreferredLayers()'); if (typeof spatialLayer !== 'number') { throw new TypeError('spatialLayer must be a number'); } if (temporalLayer && typeof temporalLayer !== 'number') { throw new TypeError('if given, temporalLayer must be a number'); } const builder = this.#channel.bufferBuilder; const preferredLayersOffset = FbsConsumer.ConsumerLayers.createConsumerLayers(builder, spatialLayer, temporalLayer !== undefined ? temporalLayer : null); const requestOffset = FbsConsumer.SetPreferredLayersRequest.createSetPreferredLayersRequest(builder, preferredLayersOffset); const response = await this.#channel.request(FbsRequest.Method.CONSUMER_SET_PREFERRED_LAYERS, FbsRequest.Body.Consumer_SetPreferredLayersRequest, requestOffset, this.#internal.consumerId); /* Decode Response. */ const data = new FbsConsumer.SetPreferredLayersResponse(); let preferredLayers; // Response is empty for non Simulcast Consumers. if (response.body(data)) { const status = data.unpack(); if (status.preferredLayers) { preferredLayers = { spatialLayer: status.preferredLayers.spatialLayer, temporalLayer: status.preferredLayers.temporalLayer !== null ? status.preferredLayers.temporalLayer : undefined, }; } } this.#preferredLayers = preferredLayers; } /** * Set priority. */ async setPriority(priority) { logger.debug('setPriority()'); if (typeof priority !== 'number' || priority < 0) { throw new TypeError('priority must be a positive number'); } const requestOffset = FbsConsumer.SetPriorityRequest.createSetPriorityRequest(this.#channel.bufferBuilder, priority); const response = await this.#channel.request(FbsRequest.Method.CONSUMER_SET_PRIORITY, FbsRequest.Body.Consumer_SetPriorityRequest, requestOffset, this.#internal.consumerId); const data = new FbsConsumer.SetPriorityResponse(); response.body(data); const status = data.unpack(); this.#priority = status.priority; } /** * Unset priority. */ async unsetPriority() { logger.debug('unsetPriority()'); await this.setPriority(1); } /** * Request a key frame to the Producer. */ async requestKeyFrame() { logger.debug('requestKeyFrame()'); await this.#channel.request(FbsRequest.Method.CONSUMER_REQUEST_KEY_FRAME, undefined, undefined, this.#internal.consumerId); } /** * Enable 'trace' event. */ 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(consumerTraceEventTypeToFbs(eventType)); } catch (error) { logger.warn('enableTraceEvent() | [error:${error}]'); } } /* Build Request. */ const requestOffset = new FbsConsumer.EnableTraceEventRequestT(fbsEventTypes).pack(this.#channel.bufferBuilder); await this.#channel.request(FbsRequest.Method.CONSUMER_ENABLE_TRACE_EVENT, FbsRequest.Body.Consumer_EnableTraceEventRequest, requestOffset, this.#internal.consumerId); } handleWorkerNotifications() { this.#channel.on(this.#internal.consumerId, (event, data) => { switch (event) { case notification_1.Event.CONSUMER_PRODUCER_CLOSE: { if (this.#closed) { break; } this.#closed = true; // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.consumerId); this.emit('@producerclose'); this.safeEmit('producerclose'); // Emit observer event. this.#observer.safeEmit('close'); break; } case notification_1.Event.CONSUMER_PRODUCER_PAUSE: { if (this.#producerPaused) { break; } this.#producerPaused = true; this.safeEmit('producerpause'); // Emit observer event. if (!this.#paused) { this.#observer.safeEmit('pause'); } break; } case notification_1.Event.CONSUMER_PRODUCER_RESUME: { if (!this.#producerPaused) { break; } this.#producerPaused = false; this.safeEmit('producerresume'); // Emit observer event. if (!this.#paused) { this.#observer.safeEmit('resume'); } break; } case notification_1.Event.CONSUMER_SCORE: { const notification = new FbsConsumer.ScoreNotification(); data.body(notification); const score = notification.score().unpack(); this.#score = score; this.safeEmit('score', score); // Emit observer event. this.#observer.safeEmit('score', score); break; } case notification_1.Event.CONSUMER_LAYERS_CHANGE: { const notification = new FbsConsumer.LayersChangeNotification(); data.body(notification); const layers = notification.layers() ? parseConsumerLayers(notification.layers()) : undefined; this.#currentLayers = layers; this.safeEmit('layerschange', layers); // Emit observer event. this.#observer.safeEmit('layerschange', layers); break; } case notification_1.Event.CONSUMER_TRACE: { const notification = new FbsConsumer.TraceNotification(); data.body(notification); const trace = parseTraceEventData(notification); this.safeEmit('trace', trace); // Emit observer event. this.observer.safeEmit('trace', trace); this.safeEmit('trace', trace); // Emit observer event. this.#observer.safeEmit('trace', trace); break; } case notification_1.Event.CONSUMER_RTP: { if (this.#closed) { break; } const notification = new FbsConsumer.RtpNotification(); data.body(notification); this.safeEmit('rtp', Buffer.from(notification.dataArray())); break; } default: { logger.error('ignoring unknown event "%s"', event); } } }); } } exports.Consumer = Consumer; function parseTraceEventData(trace) { let info; if (trace.infoType() !== FbsConsumer.TraceInfo.NONE) { const accessor = trace.info.bind(trace); info = FbsConsumerTraceInfo.unionToTraceInfo(trace.infoType(), accessor); trace.info(info); } return { type: consumerTraceEventTypeFromFbs(trace.type()), timestamp: Number(trace.timestamp()), direction: trace.direction() === common_1.TraceDirection.DIRECTION_IN ? 'in' : 'out', info: info ? info.unpack() : undefined, }; } function consumerTraceEventTypeToFbs(eventType) { switch (eventType) { case 'keyframe': { return FbsConsumer.TraceEventType.KEYFRAME; } case 'fir': { return FbsConsumer.TraceEventType.FIR; } case 'nack': { return FbsConsumer.TraceEventType.NACK; } case 'pli': { return FbsConsumer.TraceEventType.PLI; } case 'rtp': { return FbsConsumer.TraceEventType.RTP; } default: { throw new TypeError(`invalid ConsumerTraceEventType: ${eventType}`); } } } function consumerTraceEventTypeFromFbs(traceType) { switch (traceType) { case FbsConsumer.TraceEventType.KEYFRAME: { return 'keyframe'; } case FbsConsumer.TraceEventType.FIR: { return 'fir'; } case FbsConsumer.TraceEventType.NACK: { return 'nack'; } case FbsConsumer.TraceEventType.PLI: { return 'pli'; } case FbsConsumer.TraceEventType.RTP: { return 'rtp'; } default: { throw new TypeError(`invalid FbsConsumer.TraceEventType: ${traceType}`); } } } function parseConsumerLayers(data) { const spatialLayer = data.spatialLayer(); const temporalLayer = data.temporalLayer() !== null ? data.temporalLayer() : undefined; return { spatialLayer, temporalLayer, }; } function parseRtpStreamParameters(data) { return { encodingIdx: data.encodingIdx(), ssrc: data.ssrc(), payloadType: data.payloadType(), mimeType: data.mimeType(), clockRate: data.clockRate(), rid: data.rid().length > 0 ? data.rid() : undefined, cname: data.cname(), rtxSsrc: data.rtxSsrc() !== null ? data.rtxSsrc() : undefined, rtxPayloadType: data.rtxPayloadType() !== null ? data.rtxPayloadType() : undefined, useNack: data.useNack(), usePli: data.usePli(), useFir: data.useFir(), useInBandFec: data.useInBandFec(), useDtx: data.useDtx(), spatialLayers: data.spatialLayers(), temporalLayers: data.temporalLayers(), }; } function parseRtxStreamParameters(data) { return { ssrc: data.ssrc(), payloadType: data.payloadType(), mimeType: data.mimeType(), clockRate: data.clockRate(), rrid: data.rrid().length > 0 ? data.rrid() : undefined, cname: data.cname(), }; } function parseRtxStream(data) { const params = parseRtxStreamParameters(data.params()); return { params, }; } function parseRtpStream(data) { const params = parseRtpStreamParameters(data.params()); let rtxStream; if (data.rtxStream()) { rtxStream = parseRtxStream(data.rtxStream()); } return { params, score: data.score(), rtxStream, }; } function parseBaseConsumerDump(data) { return { id: data.id(), producerId: data.producerId(), kind: data.kind() === FbsRtpParameters.MediaKind.AUDIO ? 'audio' : 'video', rtpParameters: (0, RtpParameters_1.parseRtpParameters)(data.rtpParameters()), consumableRtpEncodings: data.consumableRtpEncodingsLength() > 0 ? utils.parseVector(data, 'consumableRtpEncodings', RtpParameters_1.parseRtpEncodingParameters) : undefined, traceEventTypes: utils.parseVector(data, 'traceEventTypes', consumerTraceEventTypeFromFbs), supportedCodecPayloadTypes: utils.parseVector(data, 'supportedCodecPayloadTypes'), paused: data.paused(), producerPaused: data.producerPaused(), priority: data.priority(), }; } function parseSimpleConsumerDump(data) { const base = parseBaseConsumerDump(data.base()); const rtpStream = parseRtpStream(data.rtpStreams(0)); return { ...base, type: 'simple', rtpStream, }; } function parseSimulcastConsumerDump(data) { const base = parseBaseConsumerDump(data.base()); const rtpStream = parseRtpStream(data.rtpStreams(0)); return { ...base, type: 'simulcast', rtpStream, preferredSpatialLayer: data.preferredSpatialLayer(), targetSpatialLayer: data.targetSpatialLayer(), currentSpatialLayer: data.currentSpatialLayer(), preferredTemporalLayer: data.preferredTemporalLayer(), targetTemporalLayer: data.targetTemporalLayer(), currentTemporalLayer: data.currentTemporalLayer(), }; } function parseSvcConsumerDump(data) { const dump = parseSimulcastConsumerDump(data); dump.type = 'svc'; return dump; } function parsePipeConsumerDump(data) { const base = parseBaseConsumerDump(data.base()); const rtpStreams = utils.parseVector(data, 'rtpStreams', parseRtpStream); return { ...base, type: 'pipe', rtpStreams, }; } function parseConsumerDumpResponse(data) { const type = data.data().base().type(); switch (type) { case rtp_parameters_1.Type.SIMPLE: { const dump = new FbsConsumer.ConsumerDump(); data.data(dump); return parseSimpleConsumerDump(dump); } case rtp_parameters_1.Type.SIMULCAST: { const dump = new FbsConsumer.ConsumerDump(); data.data(dump); return parseSimulcastConsumerDump(dump); } case rtp_parameters_1.Type.SVC: { const dump = new FbsConsumer.ConsumerDump(); data.data(dump); return parseSvcConsumerDump(dump); } case rtp_parameters_1.Type.PIPE: { const dump = new FbsConsumer.ConsumerDump(); data.data(dump); return parsePipeConsumerDump(dump); } default: { throw new TypeError(`invalid Consumer type: ${type}`); } } } function parseConsumerStats(binary) { return utils.parseVector(binary, 'stats', RtpStream_1.parseRtpStreamStats); }