mediasoup
Version:
Cutting Edge WebRTC Video Conferencing
540 lines (539 loc) • 20.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConsumerImpl = void 0;
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 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 ConsumerImpl 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();
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();
this.handleListenerError();
}
get id() {
return this.#internal.consumerId;
}
get producerId() {
return this.#data.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 paused() {
return this.#paused;
}
get producerPaused() {
return this.#producerPaused;
}
get priority() {
return this.#priority;
}
get score() {
return this.#score;
}
get preferredLayers() {
return this.#preferredLayers;
}
get currentLayers() {
return this.#currentLayers;
}
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.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');
}
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');
}
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);
}
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);
}
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');
}
}
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');
}
}
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 ?? 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 ?? undefined,
};
}
}
this.#preferredLayers = preferredLayers;
}
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;
}
async unsetPriority() {
logger.debug('unsetPriority()');
await this.setPriority(1);
}
async requestKeyFrame() {
logger.debug('requestKeyFrame()');
await this.#channel.request(FbsRequest.Method.CONSUMER_REQUEST_KEY_FRAME, undefined, undefined, this.#internal.consumerId);
}
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 "${event}"`);
}
}
});
}
handleListenerError() {
this.on('listenererror', (eventName, error) => {
logger.error(`event listener threw an error [eventName:${eventName}]:`, error);
});
}
}
exports.ConsumerImpl = ConsumerImpl;
function parseTraceEventData(trace) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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?.unpack(),
};
}
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 parseRtpStream(data) {
const params = parseRtpStreamParameters(data.params());
let rtxStream;
if (data.rtxStream()) {
rtxStream = parseRtxStream(data.rtxStream());
}
return {
params,
score: data.score(),
rtxStream,
};
}
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 parseRtxStream(data) {
const params = parseRtxStreamParameters(data.params());
return {
params,
};
}
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 parseBaseConsumerDump(data) {
return {
id: data.id(),
producerId: data.producerId(),
kind: data.kind() === FbsRtpParameters.MediaKind.AUDIO ? 'audio' : 'video',
rtpParameters: (0, rtpParametersFbsUtils_1.parseRtpParameters)(data.rtpParameters()),
consumableRtpEncodings: data.consumableRtpEncodingsLength() > 0
? fbsUtils.parseVector(data, 'consumableRtpEncodings', rtpParametersFbsUtils_1.parseRtpEncodingParameters)
: undefined,
traceEventTypes: fbsUtils.parseVector(data, 'traceEventTypes', consumerTraceEventTypeFromFbs),
supportedCodecPayloadTypes: fbsUtils.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 = fbsUtils.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 fbsUtils.parseVector(binary, 'stats', rtpStreamStatsFbsUtils_1.parseRtpStreamStats);
}