msc-node
Version:
mediasoup client side Node.js library
623 lines (622 loc) • 26.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Transport = void 0;
const awaitqueue_1 = require("awaitqueue");
const Logger_1 = require("./Logger");
const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter");
const errors_1 = require("./errors");
const utils = __importStar(require("./utils"));
const ortc = __importStar(require("./ortc"));
const Producer_1 = require("./Producer");
const Consumer_1 = require("./Consumer");
const DataProducer_1 = require("./DataProducer");
const DataConsumer_1 = require("./DataConsumer");
const logger = new Logger_1.Logger('Transport');
class Transport extends EnhancedEventEmitter_1.EnhancedEventEmitter {
/**
* @emits connect - (transportLocalParameters: any, callback: Function, errback: Function)
* @emits connectionstatechange - (connectionState: ConnectionState)
* @emits produce - (producerLocalParameters: any, callback: Function, errback: Function)
* @emits producedata - (dataProducerLocalParameters: any, callback: Function, errback: Function)
*/
constructor({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData, handlerFactory, extendedRtpCapabilities, canProduceByKind }) {
super();
// Closed flag.
this._closed = false;
// Transport connection state.
this._connectionState = 'new';
// Map of Producers indexed by id.
this._producers = new Map();
// Map of Consumers indexed by id.
this._consumers = new Map();
// Map of DataProducers indexed by id.
this._dataProducers = new Map();
// Map of DataConsumers indexed by id.
this._dataConsumers = new Map();
// Whether the Consumer for RTP probation has been created.
this._probatorConsumerCreated = false;
// AwaitQueue instance to make async tasks happen sequentially.
this._awaitQueue = new awaitqueue_1.AwaitQueue({ ClosedErrorClass: errors_1.InvalidStateError });
// Observer instance.
this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter();
logger.debug('constructor() [id:%s, direction:%s]', id, direction);
this._id = id;
this._direction = direction;
this._extendedRtpCapabilities = extendedRtpCapabilities;
this._canProduceByKind = canProduceByKind;
this._maxSctpMessageSize =
sctpParameters ? sctpParameters.maxMessageSize : null;
// Clone and sanitize additionalSettings.
additionalSettings = utils.clone(additionalSettings, {});
delete additionalSettings.iceServers;
delete additionalSettings.iceTransportPolicy;
delete additionalSettings.bundlePolicy;
delete additionalSettings.rtcpMuxPolicy;
delete additionalSettings.sdpSemantics;
this._handler = handlerFactory();
this._handler.run({
direction,
iceParameters,
iceCandidates,
dtlsParameters,
sctpParameters,
iceServers,
iceTransportPolicy,
additionalSettings,
proprietaryConstraints,
extendedRtpCapabilities
});
this._appData = appData;
// @ts-expect-error
this.pc = this._handler._pc;
this._handleHandler();
}
/**
* Transport id.
*/
get id() {
return this._id;
}
/**
* Whether the Transport is closed.
*/
get closed() {
return this._closed;
}
/**
* Transport direction.
*/
get direction() {
return this._direction;
}
/**
* RTC handler instance.
*/
get handler() {
return this._handler;
}
/**
* Connection state.
*/
get connectionState() {
return this._connectionState;
}
/**
* App custom data.
*/
get appData() {
return this._appData;
}
/**
* Invalid setter.
*/
set appData(appData) {
throw new Error('cannot override appData object');
}
/**
* Observer.
*
* @emits close
* @emits newproducer - (producer: Producer)
* @emits newconsumer - (producer: Producer)
* @emits newdataproducer - (dataProducer: DataProducer)
* @emits newdataconsumer - (dataProducer: DataProducer)
*/
get observer() {
return this._observer;
}
/**
* Close the Transport.
*/
close() {
if (this._closed) {
return;
}
logger.debug('close()');
this._closed = true;
// Close the AwaitQueue.
this._awaitQueue.close();
// Close the handler.
this._handler.close();
// Close all Producers.
for (const producer of this._producers.values()) {
producer.transportClosed();
}
this._producers.clear();
// Close all Consumers.
for (const consumer of this._consumers.values()) {
consumer.transportClosed();
}
this._consumers.clear();
// Close all DataProducers.
for (const dataProducer of this._dataProducers.values()) {
dataProducer.transportClosed();
}
this._dataProducers.clear();
// Close all DataConsumers.
for (const dataConsumer of this._dataConsumers.values()) {
dataConsumer.transportClosed();
}
this._dataConsumers.clear();
// Emit observer event.
this._observer.safeEmit('close');
}
/**
* Get associated Transport (RTCPeerConnection) stats.
*
* @returns {RTCStatsReport}
*/
getStats() {
return __awaiter(this, void 0, void 0, function* () {
if (this._closed) {
throw new errors_1.InvalidStateError('closed');
}
return this._handler.getTransportStats();
});
}
/**
* Restart ICE connection.
*/
restartIce({ iceParameters }) {
return __awaiter(this, void 0, void 0, function* () {
logger.debug('restartIce()');
if (this._closed) {
throw new errors_1.InvalidStateError('closed');
}
else if (!iceParameters) {
throw new TypeError('missing iceParameters');
}
// Enqueue command.
return this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () { return this._handler.restartIce(iceParameters); }), 'transport.restartIce()');
});
}
/**
* Update ICE servers.
*/
updateIceServers({ iceServers } = {}) {
return __awaiter(this, void 0, void 0, function* () {
logger.debug('updateIceServers()');
if (this._closed) {
throw new errors_1.InvalidStateError('closed');
}
else if (!Array.isArray(iceServers)) {
throw new TypeError('missing iceServers');
}
// Enqueue command.
return this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () { return this._handler.updateIceServers(iceServers); }), 'transport.updateIceServers()');
});
}
/**
* Create a Producer.
*/
produce({ track, encodings, codecOptions, codec, stopTracks = true, disableTrackOnPause = true, zeroRtpOnPause = false, appData = {} } = {}) {
return __awaiter(this, void 0, void 0, function* () {
logger.debug('produce() [track:%o]', track);
if (!track) {
throw new TypeError('missing track');
}
else if (this._direction !== 'send') {
throw new errors_1.UnsupportedError('not a sending Transport');
}
else if (!this._canProduceByKind[track.kind]) {
throw new errors_1.UnsupportedError(`cannot produce ${track.kind}`);
}
// else if (track.readyState === 'ended')
// { throw new InvalidStateError('track ended'); }
else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') {
throw new TypeError('no "connect" listener set into this transport');
}
else if (this.listenerCount('produce') === 0) {
throw new TypeError('no "produce" listener set into this transport');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
// Enqueue command.
return this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () {
let normalizedEncodings;
if (encodings && !Array.isArray(encodings)) {
throw TypeError('encodings must be an array');
}
else if (encodings && encodings.length === 0) {
normalizedEncodings = undefined;
}
else if (encodings) {
normalizedEncodings = encodings
.map((encoding) => {
const normalizedEncoding = { active: true };
if (encoding.active === false) {
normalizedEncoding.active = false;
}
if (typeof encoding.dtx === 'boolean') {
normalizedEncoding.dtx = encoding.dtx;
}
if (typeof encoding.scalabilityMode === 'string') {
normalizedEncoding.scalabilityMode = encoding.scalabilityMode;
}
if (typeof encoding.scaleResolutionDownBy === 'number') {
normalizedEncoding.scaleResolutionDownBy = encoding.scaleResolutionDownBy;
}
if (typeof encoding.maxBitrate === 'number') {
normalizedEncoding.maxBitrate = encoding.maxBitrate;
}
if (typeof encoding.maxFramerate === 'number') {
normalizedEncoding.maxFramerate = encoding.maxFramerate;
}
if (typeof encoding.adaptivePtime === 'boolean') {
normalizedEncoding.adaptivePtime = encoding.adaptivePtime;
}
if (typeof encoding.priority === 'string') {
normalizedEncoding.priority = encoding.priority;
}
if (typeof encoding.networkPriority === 'string') {
normalizedEncoding.networkPriority = encoding.networkPriority;
}
return normalizedEncoding;
});
}
const { localId, rtpParameters, rtpSender } = yield this._handler.send({
track,
encodings: normalizedEncodings,
codecOptions,
codec
});
try {
// This will fill rtpParameters's missing fields with default values.
ortc.validateRtpParameters(rtpParameters);
const { id } = yield this.safeEmitAsPromise('produce', {
kind: track.kind,
rtpParameters,
appData
});
const producer = new Producer_1.Producer({
id,
localId,
rtpSender,
track,
rtpParameters,
stopTracks,
disableTrackOnPause,
zeroRtpOnPause,
appData
});
this._producers.set(producer.id, producer);
this._handleProducer(producer);
// Emit observer event.
this._observer.safeEmit('newproducer', producer);
return producer;
}
catch (error) {
this._handler.stopSending(localId)
.catch(() => { });
throw error;
}
}), 'transport.produce()')
// This catch is needed to stop the given track if the command above
// failed due to closed Transport.
.catch((error) => {
if (stopTracks) {
try {
track.stop();
}
catch (error2) { }
}
throw error;
});
});
}
/**
* Create a Consumer to consume a remote Producer.
*/
consume({ id, producerId, kind, rtpParameters, appData = {} }) {
return __awaiter(this, void 0, void 0, function* () {
logger.debug('consume()');
rtpParameters = utils.clone(rtpParameters, undefined);
if (this._closed) {
throw new errors_1.InvalidStateError('closed');
}
else if (this._direction !== 'recv') {
throw new errors_1.UnsupportedError('not a receiving Transport');
}
else if (typeof id !== 'string') {
throw new TypeError('missing id');
}
else if (typeof producerId !== 'string') {
throw new TypeError('missing producerId');
}
else if (kind !== 'audio' && kind !== 'video') {
throw new TypeError(`invalid kind '${kind}'`);
}
else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') {
throw new TypeError('no "connect" listener set into this transport');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
// Enqueue command.
return this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () {
// Ensure the device can consume it.
const canConsume = ortc.canReceive(rtpParameters, this._extendedRtpCapabilities);
if (!canConsume) {
throw new errors_1.UnsupportedError('cannot consume this Producer');
}
const { localId, rtpReceiver, track } = yield this._handler.receive({ trackId: id, kind, rtpParameters });
const consumer = new Consumer_1.Consumer({
id,
localId,
producerId,
rtpReceiver,
track: track,
rtpParameters,
appData
});
this._consumers.set(consumer.id, consumer);
this._handleConsumer(consumer);
// If this is the first video Consumer and the Consumer for RTP probation
// has not yet been created, create it now.
if (!this._probatorConsumerCreated && kind === 'video') {
try {
const probatorRtpParameters = ortc.generateProbatorRtpParameters(consumer.rtpParameters);
yield this._handler.receive({
trackId: 'probator',
kind: 'video',
rtpParameters: probatorRtpParameters
});
logger.debug('consume() | Consumer for RTP probation created');
this._probatorConsumerCreated = true;
}
catch (error) {
logger.error('consume() | failed to create Consumer for RTP probation:%o', error);
}
}
// Emit observer event.
this._observer.safeEmit('newconsumer', consumer);
return consumer;
}), 'transport.consume()');
});
}
/**
* Create a DataProducer
*/
produceData({ ordered = true, maxPacketLifeTime, maxRetransmits, priority = 'low', label = '', protocol = '', appData = {} } = {}) {
return __awaiter(this, void 0, void 0, function* () {
logger.debug('produceData()');
if (this._direction !== 'send') {
throw new errors_1.UnsupportedError('not a sending Transport');
}
else if (!this._maxSctpMessageSize) {
throw new errors_1.UnsupportedError('SCTP not enabled by remote Transport');
}
else if (!['very-low', 'low', 'medium', 'high'].includes(priority)) {
throw new TypeError('wrong priority');
}
else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') {
throw new TypeError('no "connect" listener set into this transport');
}
else if (this.listenerCount('producedata') === 0) {
throw new TypeError('no "producedata" listener set into this transport');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
if (maxPacketLifeTime || maxRetransmits) {
ordered = false;
}
// Enqueue command.
return this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () {
const { dataChannel, sctpStreamParameters } = yield this._handler.sendDataChannel({
ordered,
maxPacketLifeTime,
maxRetransmits,
priority,
label,
protocol
});
// This will fill sctpStreamParameters's missing fields with default values.
ortc.validateSctpStreamParameters(sctpStreamParameters);
const { id } = yield this.safeEmitAsPromise('producedata', {
sctpStreamParameters,
label,
protocol,
appData
});
const dataProducer = new DataProducer_1.DataProducer({ id, dataChannel, sctpStreamParameters, appData });
this._dataProducers.set(dataProducer.id, dataProducer);
this._handleDataProducer(dataProducer);
// Emit observer event.
this._observer.safeEmit('newdataproducer', dataProducer);
return dataProducer;
}), 'transport.produceData()');
});
}
/**
* Create a DataConsumer
*/
consumeData({ id, dataProducerId, sctpStreamParameters, label = '', protocol = '', appData = {} }) {
return __awaiter(this, void 0, void 0, function* () {
logger.debug('consumeData()');
sctpStreamParameters = utils.clone(sctpStreamParameters, undefined);
if (this._closed) {
throw new errors_1.InvalidStateError('closed');
}
else if (this._direction !== 'recv') {
throw new errors_1.UnsupportedError('not a receiving Transport');
}
else if (!this._maxSctpMessageSize) {
throw new errors_1.UnsupportedError('SCTP not enabled by remote Transport');
}
else if (typeof id !== 'string') {
throw new TypeError('missing id');
}
else if (typeof dataProducerId !== 'string') {
throw new TypeError('missing dataProducerId');
}
else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') {
throw new TypeError('no "connect" listener set into this transport');
}
else if (appData && typeof appData !== 'object') {
throw new TypeError('if given, appData must be an object');
}
// This may throw.
ortc.validateSctpStreamParameters(sctpStreamParameters);
// Enqueue command.
return this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () {
const { dataChannel } = yield this._handler.receiveDataChannel({
sctpStreamParameters,
label,
protocol
});
const dataConsumer = new DataConsumer_1.DataConsumer({
id,
dataProducerId,
dataChannel,
sctpStreamParameters,
appData
});
this._dataConsumers.set(dataConsumer.id, dataConsumer);
this._handleDataConsumer(dataConsumer);
// Emit observer event.
this._observer.safeEmit('newdataconsumer', dataConsumer);
return dataConsumer;
}), 'transport.consumeData()');
});
}
_handleHandler() {
const handler = this._handler;
handler.on('@connect', ({ dtlsParameters }, callback, errback) => {
if (this._closed) {
errback(new errors_1.InvalidStateError('closed'));
return;
}
this.safeEmit('connect', { dtlsParameters }, callback, errback);
});
handler.on('@connectionstatechange', (connectionState) => {
if (connectionState === this._connectionState) {
return;
}
logger.debug('connection state changed to %s', connectionState);
this._connectionState = connectionState;
if (!this._closed) {
this.safeEmit('connectionstatechange', connectionState);
}
});
}
_handleProducer(producer) {
producer.on('@close', () => {
this._producers.delete(producer.id);
if (this._closed) {
return;
}
this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () { return this._handler.stopSending(producer.localId); }), 'producer @close event')
.catch((error) => logger.warn('producer.close() failed:%o', error));
});
producer.on('@replacetrack', (track, callback, errback) => {
this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () { return this._handler.replaceTrack(producer.localId, track); }), 'producer @replacetrack event')
.then(callback)
.catch(errback);
});
producer.on('@setmaxspatiallayer', (spatialLayer, callback, errback) => {
this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () {
return (this._handler.setMaxSpatialLayer(producer.localId, spatialLayer));
}), 'producer @setmaxspatiallayer event')
.then(callback)
.catch(errback);
});
producer.on('@setrtpencodingparameters', (params, callback, errback) => {
this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () {
return (this._handler.setRtpEncodingParameters(producer.localId, params));
}), 'producer @setrtpencodingparameters event')
.then(callback)
.catch(errback);
});
producer.on('@getstats', (callback, errback) => {
if (this._closed) {
return errback(new errors_1.InvalidStateError('closed'));
}
this._handler.getSenderStats(producer.localId)
.then(callback)
.catch(errback);
});
}
_handleConsumer(consumer) {
consumer.on('@close', () => {
this._consumers.delete(consumer.id);
if (this._closed) {
return;
}
this._awaitQueue.push(() => __awaiter(this, void 0, void 0, function* () { return this._handler.stopReceiving(consumer.localId); }), 'consumer @close event')
.catch(() => { });
});
consumer.on('@getstats', (callback, errback) => {
if (this._closed) {
return errback(new errors_1.InvalidStateError('closed'));
}
this._handler.getReceiverStats(consumer.localId)
.then(callback)
.catch(errback);
});
}
_handleDataProducer(dataProducer) {
dataProducer.on('@close', () => {
this._dataProducers.delete(dataProducer.id);
});
}
_handleDataConsumer(dataConsumer) {
dataConsumer.on('@close', () => {
this._dataConsumers.delete(dataConsumer.id);
});
}
}
exports.Transport = Transport;