mediasoup
Version:
Cutting Edge WebRTC Video Conferencing
825 lines (824 loc) • 39.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Router = void 0;
exports.parseRouterDumpResponse = parseRouterDumpResponse;
const Logger_1 = require("./Logger");
const enhancedEvents_1 = require("./enhancedEvents");
const ortc = require("./ortc");
const errors_1 = require("./errors");
const Transport_1 = require("./Transport");
const WebRtcTransport_1 = require("./WebRtcTransport");
const PlainTransport_1 = require("./PlainTransport");
const PipeTransport_1 = require("./PipeTransport");
const DirectTransport_1 = require("./DirectTransport");
const ActiveSpeakerObserver_1 = require("./ActiveSpeakerObserver");
const AudioLevelObserver_1 = require("./AudioLevelObserver");
const SrtpParameters_1 = require("./SrtpParameters");
const utils_1 = require("./utils");
const FbsActiveSpeakerObserver = require("./fbs/active-speaker-observer");
const FbsAudioLevelObserver = require("./fbs/audio-level-observer");
const FbsRequest = require("./fbs/request");
const FbsWorker = require("./fbs/worker");
const FbsRouter = require("./fbs/router");
const FbsTransport = require("./fbs/transport");
const protocol_1 = require("./fbs/transport/protocol");
const FbsWebRtcTransport = require("./fbs/web-rtc-transport");
const FbsPlainTransport = require("./fbs/plain-transport");
const FbsPipeTransport = require("./fbs/pipe-transport");
const FbsDirectTransport = require("./fbs/direct-transport");
const FbsSctpParameters = require("./fbs/sctp-parameters");
const logger = new Logger_1.Logger('Router');
class Router extends enhancedEvents_1.EnhancedEventEmitter {
// Internal data.
#internal;
// Router data.
#data;
// Channel instance.
#channel;
// Closed flag.
#closed = false;
// Custom app data.
#appData;
// Transports map.
#transports = new Map();
// Producers map.
#producers = new Map();
// RtpObservers map.
#rtpObservers = new Map();
// DataProducers map.
#dataProducers = new Map();
// Map of PipeTransport pair Promises indexed by the id of the Router in
// which pipeToRouter() was called.
#mapRouterPairPipeTransportPairPromise = new Map();
// Observer instance.
#observer = new enhancedEvents_1.EnhancedEventEmitter();
/**
* @private
*/
constructor({ internal, data, channel, appData, }) {
super();
logger.debug('constructor()');
this.#internal = internal;
this.#data = data;
this.#channel = channel;
this.#appData = appData || {};
}
/**
* Router id.
*/
get id() {
return this.#internal.routerId;
}
/**
* Whether the Router is closed.
*/
get closed() {
return this.#closed;
}
/**
* RTP capabilities of the Router.
*/
get rtpCapabilities() {
return this.#data.rtpCapabilities;
}
/**
* 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 transportsForTesting() {
return this.#transports;
}
/**
* Close the Router.
*/
close() {
if (this.#closed) {
return;
}
logger.debug('close()');
this.#closed = true;
const requestOffset = new FbsWorker.CloseRouterRequestT(this.#internal.routerId).pack(this.#channel.bufferBuilder);
this.#channel
.request(FbsRequest.Method.WORKER_CLOSE_ROUTER, FbsRequest.Body.Worker_CloseRouterRequest, requestOffset)
.catch(() => { });
// Close every Transport.
for (const transport of this.#transports.values()) {
transport.routerClosed();
}
this.#transports.clear();
// Clear the Producers map.
this.#producers.clear();
// Close every RtpObserver.
for (const rtpObserver of this.#rtpObservers.values()) {
rtpObserver.routerClosed();
}
this.#rtpObservers.clear();
// Clear the DataProducers map.
this.#dataProducers.clear();
this.emit('@close');
// Emit observer event.
this.#observer.safeEmit('close');
}
/**
* Worker was closed.
*
* @private
*/
workerClosed() {
if (this.#closed) {
return;
}
logger.debug('workerClosed()');
this.#closed = true;
// Close every Transport.
for (const transport of this.#transports.values()) {
transport.routerClosed();
}
this.#transports.clear();
// Clear the Producers map.
this.#producers.clear();
// Close every RtpObserver.
for (const rtpObserver of this.#rtpObservers.values()) {
rtpObserver.routerClosed();
}
this.#rtpObservers.clear();
// Clear the DataProducers map.
this.#dataProducers.clear();
this.safeEmit('workerclose');
// Emit observer event.
this.#observer.safeEmit('close');
}
/**
* Dump Router.
*/
async dump() {
logger.debug('dump()');
// Send the request and wait for the response.
const response = await this.#channel.request(FbsRequest.Method.ROUTER_DUMP, undefined, undefined, this.#internal.routerId);
/* Decode Response. */
const dump = new FbsRouter.DumpResponse();
response.body(dump);
return parseRouterDumpResponse(dump);
}
/**
* Create a WebRtcTransport.
*/
async createWebRtcTransport({ webRtcServer, listenInfos, listenIps, port, enableUdp, enableTcp, preferUdp = false, preferTcp = false, initialAvailableOutgoingBitrate = 600000, enableSctp = false, numSctpStreams = { OS: 1024, MIS: 1024 }, maxSctpMessageSize = 262144, sctpSendBufferSize = 262144, iceConsentTimeout = 30, appData, }) {
logger.debug('createWebRtcTransport()');
if (!webRtcServer &&
!Array.isArray(listenInfos) &&
!Array.isArray(listenIps)) {
throw new TypeError('missing webRtcServer, listenInfos and listenIps (one of them is mandatory)');
}
else if (webRtcServer && listenInfos && listenIps) {
throw new TypeError('only one of webRtcServer, listenInfos and listenIps must be given');
}
else if (numSctpStreams &&
(typeof numSctpStreams.OS !== 'number' ||
typeof numSctpStreams.MIS !== 'number')) {
throw new TypeError('if given, numSctpStreams must contain OS and MIS');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
// If webRtcServer is given, then do not force default values for enableUdp
// and enableTcp. Otherwise set them if unset.
if (webRtcServer) {
enableUdp ??= true;
enableTcp ??= true;
}
else {
enableUdp ??= true;
enableTcp ??= false;
}
// Convert deprecated TransportListenIps to TransportListenInfos.
if (listenIps) {
// Normalize IP strings to TransportListenIp objects.
listenIps = listenIps.map(listenIp => {
if (typeof listenIp === 'string') {
return { ip: listenIp };
}
else {
return listenIp;
}
});
listenInfos = [];
const orderedProtocols = [];
if (enableUdp && (preferUdp || !enableTcp || !preferTcp)) {
orderedProtocols.push('udp');
if (enableTcp) {
orderedProtocols.push('tcp');
}
}
else if (enableTcp && ((preferTcp && !preferUdp) || !enableUdp)) {
orderedProtocols.push('tcp');
if (enableUdp) {
orderedProtocols.push('udp');
}
}
for (const listenIp of listenIps) {
for (const protocol of orderedProtocols) {
listenInfos.push({
protocol: protocol,
ip: listenIp.ip,
announcedAddress: listenIp.announcedIp,
port: port,
});
}
}
}
const transportId = (0, utils_1.generateUUIDv4)();
/* Build Request. */
let webRtcTransportListenServer;
let webRtcTransportListenIndividual;
if (webRtcServer) {
webRtcTransportListenServer = new FbsWebRtcTransport.ListenServerT(webRtcServer.id);
}
else {
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));
}
webRtcTransportListenIndividual =
new FbsWebRtcTransport.ListenIndividualT(fbsListenInfos);
}
const baseTransportOptions = new FbsTransport.OptionsT(undefined /* direct */, undefined /* maxMessageSize */, initialAvailableOutgoingBitrate, enableSctp, new FbsSctpParameters.NumSctpStreamsT(numSctpStreams.OS, numSctpStreams.MIS), maxSctpMessageSize, sctpSendBufferSize, true /* isDataChannel */);
const webRtcTransportOptions = new FbsWebRtcTransport.WebRtcTransportOptionsT(baseTransportOptions, webRtcServer
? FbsWebRtcTransport.Listen.ListenServer
: FbsWebRtcTransport.Listen.ListenIndividual, webRtcServer
? webRtcTransportListenServer
: webRtcTransportListenIndividual, enableUdp, enableTcp, preferUdp, preferTcp, iceConsentTimeout);
const requestOffset = new FbsRouter.CreateWebRtcTransportRequestT(transportId, webRtcTransportOptions).pack(this.#channel.bufferBuilder);
const response = await this.#channel.request(webRtcServer
? FbsRequest.Method.ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER
: FbsRequest.Method.ROUTER_CREATE_WEBRTCTRANSPORT, FbsRequest.Body.Router_CreateWebRtcTransportRequest, requestOffset, this.#internal.routerId);
/* Decode Response. */
const data = new FbsWebRtcTransport.DumpResponse();
response.body(data);
const webRtcTransportData = (0, WebRtcTransport_1.parseWebRtcTransportDumpResponse)(data);
const transport = new WebRtcTransport_1.WebRtcTransport({
internal: {
...this.#internal,
transportId: transportId,
},
data: webRtcTransportData,
channel: this.#channel,
appData,
getRouterRtpCapabilities: () => this.#data.rtpCapabilities,
getProducerById: (producerId) => this.#producers.get(producerId),
getDataProducerById: (dataProducerId) => this.#dataProducers.get(dataProducerId),
});
this.#transports.set(transport.id, transport);
transport.on('@close', () => this.#transports.delete(transport.id));
transport.on('@listenserverclose', () => this.#transports.delete(transport.id));
transport.on('@newproducer', (producer) => this.#producers.set(producer.id, producer));
transport.on('@producerclose', (producer) => this.#producers.delete(producer.id));
transport.on('@newdataproducer', (dataProducer) => this.#dataProducers.set(dataProducer.id, dataProducer));
transport.on('@dataproducerclose', (dataProducer) => this.#dataProducers.delete(dataProducer.id));
// Emit observer event.
this.#observer.safeEmit('newtransport', transport);
if (webRtcServer) {
webRtcServer.handleWebRtcTransport(transport);
}
return transport;
}
/**
* Create a PlainTransport.
*/
async createPlainTransport({ listenInfo, rtcpListenInfo, listenIp, port, rtcpMux = true, comedia = false, enableSctp = false, numSctpStreams = { OS: 1024, MIS: 1024 }, maxSctpMessageSize = 262144, sctpSendBufferSize = 262144, enableSrtp = false, srtpCryptoSuite = 'AES_CM_128_HMAC_SHA1_80', appData, }) {
logger.debug('createPlainTransport()');
if (!listenInfo && !listenIp) {
throw new TypeError('missing listenInfo and listenIp (one of them is mandatory)');
}
else if (listenInfo && listenIp) {
throw new TypeError('only one of listenInfo and listenIp must be given');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
// If rtcpMux is enabled, ignore rtcpListenInfo.
if (rtcpMux && rtcpListenInfo) {
logger.warn('createPlainTransport() | ignoring rtcpMux since rtcpListenInfo is given');
rtcpMux = false;
}
// Convert deprecated TransportListenIps to TransportListenInfos.
if (listenIp) {
// Normalize IP string to TransportListenIp object.
if (typeof listenIp === 'string') {
listenIp = { ip: listenIp };
}
listenInfo = {
protocol: 'udp',
ip: listenIp.ip,
announcedAddress: listenIp.announcedIp,
port: port,
};
}
const transportId = (0, utils_1.generateUUIDv4)();
/* Build Request. */
const baseTransportOptions = new FbsTransport.OptionsT(undefined /* direct */, undefined /* maxMessageSize */, undefined /* initialAvailableOutgoingBitrate */, enableSctp, new FbsSctpParameters.NumSctpStreamsT(numSctpStreams.OS, numSctpStreams.MIS), maxSctpMessageSize, sctpSendBufferSize, false /* isDataChannel */);
const plainTransportOptions = new FbsPlainTransport.PlainTransportOptionsT(baseTransportOptions, 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), rtcpListenInfo
? new FbsTransport.ListenInfoT(rtcpListenInfo.protocol === 'udp'
? protocol_1.Protocol.UDP
: protocol_1.Protocol.TCP, rtcpListenInfo.ip, rtcpListenInfo.announcedAddress ?? rtcpListenInfo.announcedIp, rtcpListenInfo.port, (0, Transport_1.portRangeToFbs)(rtcpListenInfo.portRange), (0, Transport_1.socketFlagsToFbs)(rtcpListenInfo.flags), rtcpListenInfo.sendBufferSize, rtcpListenInfo.recvBufferSize)
: undefined, rtcpMux, comedia, enableSrtp, (0, SrtpParameters_1.cryptoSuiteToFbs)(srtpCryptoSuite));
const requestOffset = new FbsRouter.CreatePlainTransportRequestT(transportId, plainTransportOptions).pack(this.#channel.bufferBuilder);
const response = await this.#channel.request(FbsRequest.Method.ROUTER_CREATE_PLAINTRANSPORT, FbsRequest.Body.Router_CreatePlainTransportRequest, requestOffset, this.#internal.routerId);
/* Decode Response. */
const data = new FbsPlainTransport.DumpResponse();
response.body(data);
const plainTransportData = (0, PlainTransport_1.parsePlainTransportDumpResponse)(data);
const transport = new PlainTransport_1.PlainTransport({
internal: {
...this.#internal,
transportId: transportId,
},
data: plainTransportData,
channel: this.#channel,
appData,
getRouterRtpCapabilities: () => this.#data.rtpCapabilities,
getProducerById: (producerId) => this.#producers.get(producerId),
getDataProducerById: (dataProducerId) => this.#dataProducers.get(dataProducerId),
});
this.#transports.set(transport.id, transport);
transport.on('@close', () => this.#transports.delete(transport.id));
transport.on('@listenserverclose', () => this.#transports.delete(transport.id));
transport.on('@newproducer', (producer) => this.#producers.set(producer.id, producer));
transport.on('@producerclose', (producer) => this.#producers.delete(producer.id));
transport.on('@newdataproducer', (dataProducer) => this.#dataProducers.set(dataProducer.id, dataProducer));
transport.on('@dataproducerclose', (dataProducer) => this.#dataProducers.delete(dataProducer.id));
// Emit observer event.
this.#observer.safeEmit('newtransport', transport);
return transport;
}
/**
* Create a PipeTransport.
*/
async createPipeTransport({ listenInfo, listenIp, port, enableSctp = false, numSctpStreams = { OS: 1024, MIS: 1024 }, maxSctpMessageSize = 268435456, sctpSendBufferSize = 268435456, enableRtx = false, enableSrtp = false, appData, }) {
logger.debug('createPipeTransport()');
if (!listenInfo && !listenIp) {
throw new TypeError('missing listenInfo and listenIp (one of them is mandatory)');
}
else if (listenInfo && listenIp) {
throw new TypeError('only one of listenInfo and listenIp must be given');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
// Convert deprecated TransportListenIps to TransportListenInfos.
if (listenIp) {
// Normalize IP string to TransportListenIp object.
if (typeof listenIp === 'string') {
listenIp = { ip: listenIp };
}
listenInfo = {
protocol: 'udp',
ip: listenIp.ip,
announcedAddress: listenIp.announcedIp,
port: port,
};
}
const transportId = (0, utils_1.generateUUIDv4)();
/* Build Request. */
const baseTransportOptions = new FbsTransport.OptionsT(undefined /* direct */, undefined /* maxMessageSize */, undefined /* initialAvailableOutgoingBitrate */, enableSctp, new FbsSctpParameters.NumSctpStreamsT(numSctpStreams.OS, numSctpStreams.MIS), maxSctpMessageSize, sctpSendBufferSize, false /* isDataChannel */);
const pipeTransportOptions = new FbsPipeTransport.PipeTransportOptionsT(baseTransportOptions, 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), enableRtx, enableSrtp);
const requestOffset = new FbsRouter.CreatePipeTransportRequestT(transportId, pipeTransportOptions).pack(this.#channel.bufferBuilder);
const response = await this.#channel.request(FbsRequest.Method.ROUTER_CREATE_PIPETRANSPORT, FbsRequest.Body.Router_CreatePipeTransportRequest, requestOffset, this.#internal.routerId);
/* Decode Response. */
const data = new FbsPipeTransport.DumpResponse();
response.body(data);
const plainTransportData = (0, PipeTransport_1.parsePipeTransportDumpResponse)(data);
const transport = new PipeTransport_1.PipeTransport({
internal: {
...this.#internal,
transportId,
},
data: plainTransportData,
channel: this.#channel,
appData,
getRouterRtpCapabilities: () => this.#data.rtpCapabilities,
getProducerById: (producerId) => this.#producers.get(producerId),
getDataProducerById: (dataProducerId) => this.#dataProducers.get(dataProducerId),
});
this.#transports.set(transport.id, transport);
transport.on('@close', () => this.#transports.delete(transport.id));
transport.on('@listenserverclose', () => this.#transports.delete(transport.id));
transport.on('@newproducer', (producer) => this.#producers.set(producer.id, producer));
transport.on('@producerclose', (producer) => this.#producers.delete(producer.id));
transport.on('@newdataproducer', (dataProducer) => this.#dataProducers.set(dataProducer.id, dataProducer));
transport.on('@dataproducerclose', (dataProducer) => this.#dataProducers.delete(dataProducer.id));
// Emit observer event.
this.#observer.safeEmit('newtransport', transport);
return transport;
}
/**
* Create a DirectTransport.
*/
async createDirectTransport({ maxMessageSize = 262144, appData, } = {
maxMessageSize: 262144,
}) {
logger.debug('createDirectTransport()');
if (typeof maxMessageSize !== 'number' || maxMessageSize < 0) {
throw new TypeError('if given, maxMessageSize must be a positive number');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
const transportId = (0, utils_1.generateUUIDv4)();
/* Build Request. */
const baseTransportOptions = new FbsTransport.OptionsT(true /* direct */, maxMessageSize, undefined /* initialAvailableOutgoingBitrate */, undefined /* enableSctp */, undefined /* numSctpStreams */, undefined /* maxSctpMessageSize */, undefined /* sctpSendBufferSize */, undefined /* isDataChannel */);
const directTransportOptions = new FbsDirectTransport.DirectTransportOptionsT(baseTransportOptions);
const requestOffset = new FbsRouter.CreateDirectTransportRequestT(transportId, directTransportOptions).pack(this.#channel.bufferBuilder);
const response = await this.#channel.request(FbsRequest.Method.ROUTER_CREATE_DIRECTTRANSPORT, FbsRequest.Body.Router_CreateDirectTransportRequest, requestOffset, this.#internal.routerId);
/* Decode Response. */
const data = new FbsDirectTransport.DumpResponse();
response.body(data);
const directTransportData = (0, DirectTransport_1.parseDirectTransportDumpResponse)(data);
const transport = new DirectTransport_1.DirectTransport({
internal: {
...this.#internal,
transportId: transportId,
},
data: directTransportData,
channel: this.#channel,
appData,
getRouterRtpCapabilities: () => this.#data.rtpCapabilities,
getProducerById: (producerId) => this.#producers.get(producerId),
getDataProducerById: (dataProducerId) => this.#dataProducers.get(dataProducerId),
});
this.#transports.set(transport.id, transport);
transport.on('@close', () => this.#transports.delete(transport.id));
transport.on('@listenserverclose', () => this.#transports.delete(transport.id));
transport.on('@newproducer', (producer) => this.#producers.set(producer.id, producer));
transport.on('@producerclose', (producer) => this.#producers.delete(producer.id));
transport.on('@newdataproducer', (dataProducer) => this.#dataProducers.set(dataProducer.id, dataProducer));
transport.on('@dataproducerclose', (dataProducer) => this.#dataProducers.delete(dataProducer.id));
// Emit observer event.
this.#observer.safeEmit('newtransport', transport);
return transport;
}
/**
* Pipes the given Producer or DataProducer into another Router in same host.
*/
async pipeToRouter({ producerId, dataProducerId, router, listenInfo, listenIp, enableSctp = true, numSctpStreams = { OS: 1024, MIS: 1024 }, enableRtx = false, enableSrtp = false, }) {
logger.debug('pipeToRouter()');
if (!listenInfo && !listenIp) {
listenInfo = {
protocol: 'udp',
ip: '127.0.0.1',
};
}
if (listenInfo && listenIp) {
throw new TypeError('only one of listenInfo and listenIp must be given');
}
else if (!producerId && !dataProducerId) {
throw new TypeError('missing producerId or dataProducerId');
}
else if (producerId && dataProducerId) {
throw new TypeError('just producerId or dataProducerId can be given');
}
else if (!router) {
throw new TypeError('Router not found');
}
else if (router === this) {
throw new TypeError('cannot use this Router as destination');
}
// Convert deprecated TransportListenIps to TransportListenInfos.
if (listenIp) {
// Normalize IP string to TransportListenIp object.
if (typeof listenIp === 'string') {
listenIp = { ip: listenIp };
}
listenInfo = {
protocol: 'udp',
ip: listenIp.ip,
announcedAddress: listenIp.announcedIp,
};
}
let producer;
let dataProducer;
if (producerId) {
producer = this.#producers.get(producerId);
if (!producer) {
throw new TypeError('Producer not found');
}
}
else if (dataProducerId) {
dataProducer = this.#dataProducers.get(dataProducerId);
if (!dataProducer) {
throw new TypeError('DataProducer not found');
}
}
const pipeTransportPairKey = router.id;
let pipeTransportPairPromise = this.#mapRouterPairPipeTransportPairPromise.get(pipeTransportPairKey);
let pipeTransportPair;
let localPipeTransport;
let remotePipeTransport;
if (pipeTransportPairPromise) {
pipeTransportPair = await pipeTransportPairPromise;
localPipeTransport = pipeTransportPair[this.id];
remotePipeTransport = pipeTransportPair[router.id];
}
else {
pipeTransportPairPromise = new Promise((resolve, reject) => {
Promise.all([
this.createPipeTransport({
listenInfo: listenInfo,
enableSctp,
numSctpStreams,
enableRtx,
enableSrtp,
}),
router.createPipeTransport({
listenInfo: listenInfo,
enableSctp,
numSctpStreams,
enableRtx,
enableSrtp,
}),
])
.then(pipeTransports => {
localPipeTransport = pipeTransports[0];
remotePipeTransport = pipeTransports[1];
})
.then(() => {
return Promise.all([
localPipeTransport.connect({
ip: remotePipeTransport.tuple.localAddress,
port: remotePipeTransport.tuple.localPort,
srtpParameters: remotePipeTransport.srtpParameters,
}),
remotePipeTransport.connect({
ip: localPipeTransport.tuple.localAddress,
port: localPipeTransport.tuple.localPort,
srtpParameters: localPipeTransport.srtpParameters,
}),
]);
})
.then(() => {
localPipeTransport.observer.on('close', () => {
remotePipeTransport.close();
this.#mapRouterPairPipeTransportPairPromise.delete(pipeTransportPairKey);
});
remotePipeTransport.observer.on('close', () => {
localPipeTransport.close();
this.#mapRouterPairPipeTransportPairPromise.delete(pipeTransportPairKey);
});
resolve({
[this.id]: localPipeTransport,
[router.id]: remotePipeTransport,
});
})
.catch(error => {
logger.error('pipeToRouter() | error creating PipeTransport pair:%o', error);
if (localPipeTransport) {
localPipeTransport.close();
}
if (remotePipeTransport) {
remotePipeTransport.close();
}
reject(error);
});
});
this.#mapRouterPairPipeTransportPairPromise.set(pipeTransportPairKey, pipeTransportPairPromise);
router.addPipeTransportPair(this.id, pipeTransportPairPromise);
await pipeTransportPairPromise;
}
if (producer) {
let pipeConsumer;
let pipeProducer;
try {
pipeConsumer = await localPipeTransport.consume({
producerId: producerId,
});
pipeProducer = await remotePipeTransport.produce({
id: producer.id,
kind: pipeConsumer.kind,
rtpParameters: pipeConsumer.rtpParameters,
paused: pipeConsumer.producerPaused,
appData: producer.appData,
});
// Ensure that the producer has not been closed in the meanwhile.
if (producer.closed) {
throw new errors_1.InvalidStateError('original Producer closed');
}
// Ensure that producer.paused has not changed in the meanwhile and, if
// so, sync the pipeProducer.
if (pipeProducer.paused !== producer.paused) {
if (producer.paused) {
await pipeProducer.pause();
}
else {
await pipeProducer.resume();
}
}
// Pipe events from the pipe Consumer to the pipe Producer.
pipeConsumer.observer.on('close', () => pipeProducer.close());
pipeConsumer.observer.on('pause', () => pipeProducer.pause());
pipeConsumer.observer.on('resume', () => pipeProducer.resume());
// Pipe events from the pipe Producer to the pipe Consumer.
pipeProducer.observer.on('close', () => pipeConsumer.close());
return { pipeConsumer, pipeProducer };
}
catch (error) {
logger.error('pipeToRouter() | error creating pipe Consumer/Producer pair:%o', error);
if (pipeConsumer) {
pipeConsumer.close();
}
if (pipeProducer) {
pipeProducer.close();
}
throw error;
}
}
else if (dataProducer) {
let pipeDataConsumer;
let pipeDataProducer;
try {
pipeDataConsumer = await localPipeTransport.consumeData({
dataProducerId: dataProducerId,
});
pipeDataProducer = await remotePipeTransport.produceData({
id: dataProducer.id,
sctpStreamParameters: pipeDataConsumer.sctpStreamParameters,
label: pipeDataConsumer.label,
protocol: pipeDataConsumer.protocol,
appData: dataProducer.appData,
});
// Ensure that the dataProducer has not been closed in the meanwhile.
if (dataProducer.closed) {
throw new errors_1.InvalidStateError('original DataProducer closed');
}
// Pipe events from the pipe DataConsumer to the pipe DataProducer.
pipeDataConsumer.observer.on('close', () => pipeDataProducer.close());
// Pipe events from the pipe DataProducer to the pipe DataConsumer.
pipeDataProducer.observer.on('close', () => pipeDataConsumer.close());
return { pipeDataConsumer, pipeDataProducer };
}
catch (error) {
logger.error('pipeToRouter() | error creating pipe DataConsumer/DataProducer pair:%o', error);
if (pipeDataConsumer) {
pipeDataConsumer.close();
}
if (pipeDataProducer) {
pipeDataProducer.close();
}
throw error;
}
}
else {
throw new Error('internal error');
}
}
/**
* @private
*/
addPipeTransportPair(pipeTransportPairKey, pipeTransportPairPromise) {
if (this.#mapRouterPairPipeTransportPairPromise.has(pipeTransportPairKey)) {
throw new Error('given pipeTransportPairKey already exists in this Router');
}
this.#mapRouterPairPipeTransportPairPromise.set(pipeTransportPairKey, pipeTransportPairPromise);
pipeTransportPairPromise
.then(pipeTransportPair => {
const localPipeTransport = pipeTransportPair[this.id];
// NOTE: No need to do any other cleanup here since that is done by the
// Router calling this method on us.
localPipeTransport.observer.on('close', () => {
this.#mapRouterPairPipeTransportPairPromise.delete(pipeTransportPairKey);
});
})
.catch(() => {
this.#mapRouterPairPipeTransportPairPromise.delete(pipeTransportPairKey);
});
}
/**
* Create an ActiveSpeakerObserver
*/
async createActiveSpeakerObserver({ interval = 300, appData, } = {}) {
logger.debug('createActiveSpeakerObserver()');
if (typeof interval !== 'number') {
throw new TypeError('if given, interval must be an number');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
const rtpObserverId = (0, utils_1.generateUUIDv4)();
/* Build Request. */
const activeRtpObserverOptions = new FbsActiveSpeakerObserver.ActiveSpeakerObserverOptionsT(interval);
const requestOffset = new FbsRouter.CreateActiveSpeakerObserverRequestT(rtpObserverId, activeRtpObserverOptions).pack(this.#channel.bufferBuilder);
await this.#channel.request(FbsRequest.Method.ROUTER_CREATE_ACTIVESPEAKEROBSERVER, FbsRequest.Body.Router_CreateActiveSpeakerObserverRequest, requestOffset, this.#internal.routerId);
const activeSpeakerObserver = new ActiveSpeakerObserver_1.ActiveSpeakerObserver({
internal: {
...this.#internal,
rtpObserverId: rtpObserverId,
},
channel: this.#channel,
appData,
getProducerById: (producerId) => this.#producers.get(producerId),
});
this.#rtpObservers.set(activeSpeakerObserver.id, activeSpeakerObserver);
activeSpeakerObserver.on('@close', () => {
this.#rtpObservers.delete(activeSpeakerObserver.id);
});
// Emit observer event.
this.#observer.safeEmit('newrtpobserver', activeSpeakerObserver);
return activeSpeakerObserver;
}
/**
* Create an AudioLevelObserver.
*/
async createAudioLevelObserver({ maxEntries = 1, threshold = -80, interval = 1000, appData, } = {}) {
logger.debug('createAudioLevelObserver()');
if (typeof maxEntries !== 'number' || maxEntries <= 0) {
throw new TypeError('if given, maxEntries must be a positive number');
}
else if (typeof threshold !== 'number' ||
threshold < -127 ||
threshold > 0) {
throw new TypeError('if given, threshole must be a negative number greater than -127');
}
else if (typeof interval !== 'number') {
throw new TypeError('if given, interval must be an number');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
const rtpObserverId = (0, utils_1.generateUUIDv4)();
/* Build Request. */
const audioLevelObserverOptions = new FbsAudioLevelObserver.AudioLevelObserverOptionsT(maxEntries, threshold, interval);
const requestOffset = new FbsRouter.CreateAudioLevelObserverRequestT(rtpObserverId, audioLevelObserverOptions).pack(this.#channel.bufferBuilder);
await this.#channel.request(FbsRequest.Method.ROUTER_CREATE_AUDIOLEVELOBSERVER, FbsRequest.Body.Router_CreateAudioLevelObserverRequest, requestOffset, this.#internal.routerId);
const audioLevelObserver = new AudioLevelObserver_1.AudioLevelObserver({
internal: {
...this.#internal,
rtpObserverId: rtpObserverId,
},
channel: this.#channel,
appData,
getProducerById: (producerId) => this.#producers.get(producerId),
});
this.#rtpObservers.set(audioLevelObserver.id, audioLevelObserver);
audioLevelObserver.on('@close', () => {
this.#rtpObservers.delete(audioLevelObserver.id);
});
// Emit observer event.
this.#observer.safeEmit('newrtpobserver', audioLevelObserver);
return audioLevelObserver;
}
/**
* Check whether the given RTP capabilities can consume the given Producer.
*/
canConsume({ producerId, rtpCapabilities, }) {
const producer = this.#producers.get(producerId);
if (!producer) {
logger.error('canConsume() | Producer with id "%s" not found', producerId);
return false;
}
// Clone given RTP capabilities to not modify input data.
const clonedRtpCapabilities = (0, utils_1.clone)(rtpCapabilities);
try {
return ortc.canConsume(producer.consumableRtpParameters, clonedRtpCapabilities);
}
catch (error) {
logger.error('canConsume() | unexpected error: %s', String(error));
return false;
}
}
}
exports.Router = Router;
function parseRouterDumpResponse(binary) {
return {
id: binary.id(),
transportIds: (0, utils_1.parseVector)(binary, 'transportIds'),
rtpObserverIds: (0, utils_1.parseVector)(binary, 'rtpObserverIds'),
mapProducerIdConsumerIds: (0, utils_1.parseStringStringArrayVector)(binary, 'mapProducerIdConsumerIds'),
mapConsumerIdProducerId: (0, utils_1.parseStringStringVector)(binary, 'mapConsumerIdProducerId'),
mapProducerIdObserverIds: (0, utils_1.parseStringStringArrayVector)(binary, 'mapProducerIdObserverIds'),
mapDataProducerIdDataConsumerIds: (0, utils_1.parseStringStringArrayVector)(binary, 'mapDataProducerIdDataConsumerIds'),
mapDataConsumerIdDataProducerId: (0, utils_1.parseStringStringVector)(binary, 'mapDataConsumerIdDataProducerId'),
};
}