msc-node
Version:
mediasoup client side Node.js library
959 lines (958 loc) • 36.2 kB
JavaScript
;
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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.canReceive = exports.canSend = exports.generateProbatorRtpParameters = exports.reduceCodecs = exports.getSendingRemoteRtpParameters = exports.getSendingRtpParameters = exports.getRecvRtpCapabilities = exports.getExtendedRtpCapabilities = exports.validateSctpStreamParameters = exports.validateSctpParameters = exports.validateNumSctpStreams = exports.validateSctpCapabilities = exports.validateRtcpParameters = exports.validateRtpEncodingParameters = exports.validateRtpHeaderExtensionParameters = exports.validateRtpCodecParameters = exports.validateRtpParameters = exports.validateRtpHeaderExtension = exports.validateRtcpFeedback = exports.validateRtpCodecCapability = exports.validateRtpCapabilities = void 0;
const h264 = __importStar(require("h264-profile-level-id"));
const utils = __importStar(require("./utils"));
const RTP_PROBATOR_MID = 'probator';
const RTP_PROBATOR_SSRC = 1234;
const RTP_PROBATOR_CODEC_PAYLOAD_TYPE = 127;
/**
* Validates RtpCapabilities. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateRtpCapabilities(caps) {
if (typeof caps !== 'object') {
throw new TypeError('caps is not an object');
}
// codecs is optional. If unset, fill with an empty array.
if (caps.codecs && !Array.isArray(caps.codecs)) {
throw new TypeError('caps.codecs is not an array');
}
else if (!caps.codecs) {
caps.codecs = [];
}
for (const codec of caps.codecs) {
validateRtpCodecCapability(codec);
}
// headerExtensions is optional. If unset, fill with an empty array.
if (caps.headerExtensions && !Array.isArray(caps.headerExtensions)) {
throw new TypeError('caps.headerExtensions is not an array');
}
else if (!caps.headerExtensions) {
caps.headerExtensions = [];
}
for (const ext of caps.headerExtensions) {
validateRtpHeaderExtension(ext);
}
}
exports.validateRtpCapabilities = validateRtpCapabilities;
/**
* Validates RtpCodecCapability. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateRtpCodecCapability(codec) {
const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i');
if (typeof codec !== 'object') {
throw new TypeError('codec is not an object');
}
// mimeType is mandatory.
if (!codec.mimeType || typeof codec.mimeType !== 'string') {
throw new TypeError('missing codec.mimeType');
}
const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType);
if (!mimeTypeMatch) {
throw new TypeError('invalid codec.mimeType');
}
// Just override kind with media component of mimeType.
codec.kind = mimeTypeMatch[1].toLowerCase();
// preferredPayloadType is optional.
if (codec.preferredPayloadType && typeof codec.preferredPayloadType !== 'number') {
throw new TypeError('invalid codec.preferredPayloadType');
}
// clockRate is mandatory.
if (typeof codec.clockRate !== 'number') {
throw new TypeError('missing codec.clockRate');
}
// channels is optional. If unset, set it to 1 (just if audio).
if (codec.kind === 'audio') {
if (typeof codec.channels !== 'number') {
codec.channels = 1;
}
}
else {
delete codec.channels;
}
// parameters is optional. If unset, set it to an empty object.
if (!codec.parameters || typeof codec.parameters !== 'object') {
codec.parameters = {};
}
for (const key of Object.keys(codec.parameters)) {
let value = codec.parameters[key];
if (value === undefined) {
codec.parameters[key] = '';
value = '';
}
if (typeof value !== 'string' && typeof value !== 'number') {
throw new TypeError(`invalid codec parameter [key:${key}s, value:${value}]`);
}
// Specific parameters validation.
if (key === 'apt') {
if (typeof value !== 'number') {
throw new TypeError('invalid codec apt parameter');
}
}
}
// rtcpFeedback is optional. If unset, set it to an empty array.
if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) {
codec.rtcpFeedback = [];
}
for (const fb of codec.rtcpFeedback) {
validateRtcpFeedback(fb);
}
}
exports.validateRtpCodecCapability = validateRtpCodecCapability;
/**
* Validates RtcpFeedback. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateRtcpFeedback(fb) {
if (typeof fb !== 'object') {
throw new TypeError('fb is not an object');
}
// type is mandatory.
if (!fb.type || typeof fb.type !== 'string') {
throw new TypeError('missing fb.type');
}
// parameter is optional. If unset set it to an empty string.
if (!fb.parameter || typeof fb.parameter !== 'string') {
fb.parameter = '';
}
}
exports.validateRtcpFeedback = validateRtcpFeedback;
/**
* Validates RtpHeaderExtension. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateRtpHeaderExtension(ext) {
if (typeof ext !== 'object') {
throw new TypeError('ext is not an object');
}
// kind is optional. If unset set it to an empty string.
if (!ext.kind || typeof ext.kind !== 'string') {
ext.kind = '';
}
if (ext.kind !== '' && ext.kind !== 'audio' && ext.kind !== 'video') {
throw new TypeError('invalid ext.kind');
}
// uri is mandatory.
if (!ext.uri || typeof ext.uri !== 'string') {
throw new TypeError('missing ext.uri');
}
// preferredId is mandatory.
if (typeof ext.preferredId !== 'number') {
throw new TypeError('missing ext.preferredId');
}
// preferredEncrypt is optional. If unset set it to false.
if (ext.preferredEncrypt && typeof ext.preferredEncrypt !== 'boolean') {
throw new TypeError('invalid ext.preferredEncrypt');
}
else if (!ext.preferredEncrypt) {
ext.preferredEncrypt = false;
}
// direction is optional. If unset set it to sendrecv.
if (ext.direction && typeof ext.direction !== 'string') {
throw new TypeError('invalid ext.direction');
}
else if (!ext.direction) {
ext.direction = 'sendrecv';
}
}
exports.validateRtpHeaderExtension = validateRtpHeaderExtension;
/**
* Validates RtpParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateRtpParameters(params) {
if (typeof params !== 'object') {
throw new TypeError('params is not an object');
}
// mid is optional.
if (params.mid && typeof params.mid !== 'string') {
throw new TypeError('params.mid is not a string');
}
// codecs is mandatory.
if (!Array.isArray(params.codecs)) {
throw new TypeError('missing params.codecs');
}
for (const codec of params.codecs) {
validateRtpCodecParameters(codec);
}
// headerExtensions is optional. If unset, fill with an empty array.
if (params.headerExtensions && !Array.isArray(params.headerExtensions)) {
throw new TypeError('params.headerExtensions is not an array');
}
else if (!params.headerExtensions) {
params.headerExtensions = [];
}
for (const ext of params.headerExtensions) {
validateRtpHeaderExtensionParameters(ext);
}
// encodings is optional. If unset, fill with an empty array.
if (params.encodings && !Array.isArray(params.encodings)) {
throw new TypeError('params.encodings is not an array');
}
else if (!params.encodings) {
params.encodings = [];
}
for (const encoding of params.encodings) {
validateRtpEncodingParameters(encoding);
}
// rtcp is optional. If unset, fill with an empty object.
if (params.rtcp && typeof params.rtcp !== 'object') {
throw new TypeError('params.rtcp is not an object');
}
else if (!params.rtcp) {
params.rtcp = {};
}
validateRtcpParameters(params.rtcp);
}
exports.validateRtpParameters = validateRtpParameters;
/**
* Validates RtpCodecParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateRtpCodecParameters(codec) {
const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i');
if (typeof codec !== 'object') {
throw new TypeError('codec is not an object');
}
// mimeType is mandatory.
if (!codec.mimeType || typeof codec.mimeType !== 'string') {
throw new TypeError('missing codec.mimeType');
}
const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType);
if (!mimeTypeMatch) {
throw new TypeError('invalid codec.mimeType');
}
// payloadType is mandatory.
if (typeof codec.payloadType !== 'number') {
throw new TypeError('missing codec.payloadType');
}
// clockRate is mandatory.
if (typeof codec.clockRate !== 'number') {
throw new TypeError('missing codec.clockRate');
}
const kind = mimeTypeMatch[1].toLowerCase();
// channels is optional. If unset, set it to 1 (just if audio).
if (kind === 'audio') {
if (typeof codec.channels !== 'number') {
codec.channels = 1;
}
}
else {
delete codec.channels;
}
// parameters is optional. If unset, set it to an empty object.
if (!codec.parameters || typeof codec.parameters !== 'object') {
codec.parameters = {};
}
for (const key of Object.keys(codec.parameters)) {
let value = codec.parameters[key];
if (value === undefined) {
codec.parameters[key] = '';
value = '';
}
if (typeof value !== 'string' && typeof value !== 'number') {
throw new TypeError(`invalid codec parameter [key:${key}s, value:${value}]`);
}
// Specific parameters validation.
if (key === 'apt') {
if (typeof value !== 'number') {
throw new TypeError('invalid codec apt parameter');
}
}
}
// rtcpFeedback is optional. If unset, set it to an empty array.
if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) {
codec.rtcpFeedback = [];
}
for (const fb of codec.rtcpFeedback) {
validateRtcpFeedback(fb);
}
}
exports.validateRtpCodecParameters = validateRtpCodecParameters;
/**
* Validates RtpHeaderExtensionParameteters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateRtpHeaderExtensionParameters(ext) {
if (typeof ext !== 'object') {
throw new TypeError('ext is not an object');
}
// uri is mandatory.
if (!ext.uri || typeof ext.uri !== 'string') {
throw new TypeError('missing ext.uri');
}
// id is mandatory.
if (typeof ext.id !== 'number') {
throw new TypeError('missing ext.id');
}
// encrypt is optional. If unset set it to false.
if (ext.encrypt && typeof ext.encrypt !== 'boolean') {
throw new TypeError('invalid ext.encrypt');
}
else if (!ext.encrypt) {
ext.encrypt = false;
}
// parameters is optional. If unset, set it to an empty object.
if (!ext.parameters || typeof ext.parameters !== 'object') {
ext.parameters = {};
}
for (const key of Object.keys(ext.parameters)) {
let value = ext.parameters[key];
if (value === undefined) {
ext.parameters[key] = '';
value = '';
}
if (typeof value !== 'string' && typeof value !== 'number') {
throw new TypeError('invalid header extension parameter');
}
}
}
exports.validateRtpHeaderExtensionParameters = validateRtpHeaderExtensionParameters;
/**
* Validates RtpEncodingParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateRtpEncodingParameters(encoding) {
if (typeof encoding !== 'object') {
throw new TypeError('encoding is not an object');
}
// ssrc is optional.
if (encoding.ssrc && typeof encoding.ssrc !== 'number') {
throw new TypeError('invalid encoding.ssrc');
}
// rid is optional.
if (encoding.rid && typeof encoding.rid !== 'string') {
throw new TypeError('invalid encoding.rid');
}
// rtx is optional.
if (encoding.rtx && typeof encoding.rtx !== 'object') {
throw new TypeError('invalid encoding.rtx');
}
else if (encoding.rtx) {
// RTX ssrc is mandatory if rtx is present.
if (typeof encoding.rtx.ssrc !== 'number') {
throw new TypeError('missing encoding.rtx.ssrc');
}
}
// dtx is optional. If unset set it to false.
if (!encoding.dtx || typeof encoding.dtx !== 'boolean') {
encoding.dtx = false;
}
// scalabilityMode is optional.
if (encoding.scalabilityMode && typeof encoding.scalabilityMode !== 'string') {
throw new TypeError('invalid encoding.scalabilityMode');
}
}
exports.validateRtpEncodingParameters = validateRtpEncodingParameters;
/**
* Validates RtcpParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateRtcpParameters(rtcp) {
if (typeof rtcp !== 'object') {
throw new TypeError('rtcp is not an object');
}
// cname is optional.
if (rtcp.cname && typeof rtcp.cname !== 'string') {
throw new TypeError('invalid rtcp.cname');
}
// reducedSize is optional. If unset set it to true.
if (!rtcp.reducedSize || typeof rtcp.reducedSize !== 'boolean') {
rtcp.reducedSize = true;
}
}
exports.validateRtcpParameters = validateRtcpParameters;
/**
* Validates SctpCapabilities. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateSctpCapabilities(caps) {
if (typeof caps !== 'object') {
throw new TypeError('caps is not an object');
}
// numStreams is mandatory.
if (!caps.numStreams || typeof caps.numStreams !== 'object') {
throw new TypeError('missing caps.numStreams');
}
validateNumSctpStreams(caps.numStreams);
}
exports.validateSctpCapabilities = validateSctpCapabilities;
/**
* Validates NumSctpStreams. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateNumSctpStreams(numStreams) {
if (typeof numStreams !== 'object') {
throw new TypeError('numStreams is not an object');
}
// OS is mandatory.
if (typeof numStreams.OS !== 'number') {
throw new TypeError('missing numStreams.OS');
}
// MIS is mandatory.
if (typeof numStreams.MIS !== 'number') {
throw new TypeError('missing numStreams.MIS');
}
}
exports.validateNumSctpStreams = validateNumSctpStreams;
/**
* Validates SctpParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateSctpParameters(params) {
if (typeof params !== 'object') {
throw new TypeError('params is not an object');
}
// port is mandatory.
if (typeof params.port !== 'number') {
throw new TypeError('missing params.port');
}
// OS is mandatory.
if (typeof params.OS !== 'number') {
throw new TypeError('missing params.OS');
}
// MIS is mandatory.
if (typeof params.MIS !== 'number') {
throw new TypeError('missing params.MIS');
}
// maxMessageSize is mandatory.
if (typeof params.maxMessageSize !== 'number') {
throw new TypeError('missing params.maxMessageSize');
}
}
exports.validateSctpParameters = validateSctpParameters;
/**
* Validates SctpStreamParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateSctpStreamParameters(params) {
if (typeof params !== 'object') {
throw new TypeError('params is not an object');
}
// streamId is mandatory.
if (typeof params.streamId !== 'number') {
throw new TypeError('missing params.streamId');
}
// ordered is optional.
let orderedGiven = false;
if (typeof params.ordered === 'boolean') {
orderedGiven = true;
}
else {
params.ordered = true;
}
// maxPacketLifeTime is optional.
if (params.maxPacketLifeTime && typeof params.maxPacketLifeTime !== 'number') {
throw new TypeError('invalid params.maxPacketLifeTime');
}
// maxRetransmits is optional.
if (params.maxRetransmits && typeof params.maxRetransmits !== 'number') {
throw new TypeError('invalid params.maxRetransmits');
}
if (params.maxPacketLifeTime && params.maxRetransmits) {
throw new TypeError('cannot provide both maxPacketLifeTime and maxRetransmits');
}
if (orderedGiven &&
params.ordered &&
(params.maxPacketLifeTime || params.maxRetransmits)) {
throw new TypeError('cannot be ordered with maxPacketLifeTime or maxRetransmits');
}
else if (!orderedGiven && (params.maxPacketLifeTime || params.maxRetransmits)) {
params.ordered = false;
}
// priority is optional.
if (params.priority && typeof params.priority !== 'string') {
throw new TypeError('invalid params.priority');
}
// label is optional.
if (params.label && typeof params.label !== 'string') {
throw new TypeError('invalid params.label');
}
// protocol is optional.
if (params.protocol && typeof params.protocol !== 'string') {
throw new TypeError('invalid params.protocol');
}
}
exports.validateSctpStreamParameters = validateSctpStreamParameters;
/**
* Generate extended RTP capabilities for sending and receiving.
*/
function getExtendedRtpCapabilities(localCaps, remoteCaps) {
const extendedRtpCapabilities = {
codecs: [],
headerExtensions: []
};
// Match media codecs and keep the order preferred by remoteCaps.
for (const remoteCodec of remoteCaps.codecs || []) {
if (isRtxCodec(remoteCodec)) {
continue;
}
const matchingLocalCodec = (localCaps.codecs || [])
.find((localCodec) => (matchCodecs(localCodec, remoteCodec, { strict: true, modify: true })));
if (!matchingLocalCodec) {
continue;
}
const extendedCodec = {
mimeType: matchingLocalCodec.mimeType,
kind: matchingLocalCodec.kind,
clockRate: matchingLocalCodec.clockRate,
channels: matchingLocalCodec.channels,
localPayloadType: matchingLocalCodec.preferredPayloadType,
localRtxPayloadType: undefined,
remotePayloadType: remoteCodec.preferredPayloadType,
remoteRtxPayloadType: undefined,
localParameters: matchingLocalCodec.parameters,
remoteParameters: remoteCodec.parameters,
rtcpFeedback: reduceRtcpFeedback(matchingLocalCodec, remoteCodec)
};
extendedRtpCapabilities.codecs.push(extendedCodec);
}
// Match RTX codecs.
for (const extendedCodec of extendedRtpCapabilities.codecs) {
const matchingLocalRtxCodec = localCaps.codecs
.find((localCodec) => (isRtxCodec(localCodec) &&
localCodec.parameters.apt === extendedCodec.localPayloadType));
const matchingRemoteRtxCodec = remoteCaps.codecs
.find((remoteCodec) => (isRtxCodec(remoteCodec) &&
remoteCodec.parameters.apt === extendedCodec.remotePayloadType));
if (matchingLocalRtxCodec && matchingRemoteRtxCodec) {
extendedCodec.localRtxPayloadType = matchingLocalRtxCodec.preferredPayloadType;
extendedCodec.remoteRtxPayloadType = matchingRemoteRtxCodec.preferredPayloadType;
}
}
// Match header extensions.
for (const remoteExt of remoteCaps.headerExtensions) {
const matchingLocalExt = localCaps.headerExtensions
.find((localExt) => (matchHeaderExtensions(localExt, remoteExt)));
if (!matchingLocalExt) {
continue;
}
const extendedExt = {
kind: remoteExt.kind,
uri: remoteExt.uri,
sendId: matchingLocalExt.preferredId,
recvId: remoteExt.preferredId,
encrypt: matchingLocalExt.preferredEncrypt,
direction: 'sendrecv'
};
switch (remoteExt.direction) {
case 'sendrecv':
extendedExt.direction = 'sendrecv';
break;
case 'recvonly':
extendedExt.direction = 'sendonly';
break;
case 'sendonly':
extendedExt.direction = 'recvonly';
break;
case 'inactive':
extendedExt.direction = 'inactive';
break;
}
extendedRtpCapabilities.headerExtensions.push(extendedExt);
}
return extendedRtpCapabilities;
}
exports.getExtendedRtpCapabilities = getExtendedRtpCapabilities;
/**
* Generate RTP capabilities for receiving media based on the given extended
* RTP capabilities.
*/
function getRecvRtpCapabilities(extendedRtpCapabilities) {
const rtpCapabilities = {
codecs: [],
headerExtensions: []
};
for (const extendedCodec of extendedRtpCapabilities.codecs) {
const codec = {
mimeType: extendedCodec.mimeType,
kind: extendedCodec.kind,
preferredPayloadType: extendedCodec.remotePayloadType,
clockRate: extendedCodec.clockRate,
channels: extendedCodec.channels,
parameters: extendedCodec.localParameters,
rtcpFeedback: extendedCodec.rtcpFeedback
};
rtpCapabilities.codecs.push(codec);
// Add RTX codec.
if (!extendedCodec.remoteRtxPayloadType) {
continue;
}
const rtxCodec = {
mimeType: `${extendedCodec.kind}/rtx`,
kind: extendedCodec.kind,
preferredPayloadType: extendedCodec.remoteRtxPayloadType,
clockRate: extendedCodec.clockRate,
parameters: {
apt: extendedCodec.remotePayloadType
},
rtcpFeedback: []
};
rtpCapabilities.codecs.push(rtxCodec);
// TODO: In the future, we need to add FEC, CN, etc, codecs.
}
for (const extendedExtension of extendedRtpCapabilities.headerExtensions) {
// Ignore RTP extensions not valid for receiving.
if (extendedExtension.direction !== 'sendrecv' &&
extendedExtension.direction !== 'recvonly') {
continue;
}
const ext = {
kind: extendedExtension.kind,
uri: extendedExtension.uri,
preferredId: extendedExtension.recvId,
preferredEncrypt: extendedExtension.encrypt,
direction: extendedExtension.direction
};
rtpCapabilities.headerExtensions.push(ext);
}
return rtpCapabilities;
}
exports.getRecvRtpCapabilities = getRecvRtpCapabilities;
/**
* Generate RTP parameters of the given kind for sending media.
* NOTE: mid, encodings and rtcp fields are left empty.
*/
function getSendingRtpParameters(kind, extendedRtpCapabilities) {
const rtpParameters = {
mid: undefined,
codecs: [],
headerExtensions: [],
encodings: [],
rtcp: {}
};
for (const extendedCodec of extendedRtpCapabilities.codecs) {
if (extendedCodec.kind !== kind) {
continue;
}
const codec = {
mimeType: extendedCodec.mimeType,
payloadType: extendedCodec.localPayloadType,
clockRate: extendedCodec.clockRate,
channels: extendedCodec.channels,
parameters: extendedCodec.localParameters,
rtcpFeedback: extendedCodec.rtcpFeedback
};
rtpParameters.codecs.push(codec);
// Add RTX codec.
if (extendedCodec.localRtxPayloadType) {
const rtxCodec = {
mimeType: `${extendedCodec.kind}/rtx`,
payloadType: extendedCodec.localRtxPayloadType,
clockRate: extendedCodec.clockRate,
parameters: {
apt: extendedCodec.localPayloadType
},
rtcpFeedback: []
};
rtpParameters.codecs.push(rtxCodec);
}
}
for (const extendedExtension of extendedRtpCapabilities.headerExtensions) {
// Ignore RTP extensions of a different kind and those not valid for sending.
if ((extendedExtension.kind && extendedExtension.kind !== kind) ||
(extendedExtension.direction !== 'sendrecv' &&
extendedExtension.direction !== 'sendonly')) {
continue;
}
const ext = {
uri: extendedExtension.uri,
id: extendedExtension.sendId,
encrypt: extendedExtension.encrypt,
parameters: {}
};
rtpParameters.headerExtensions.push(ext);
}
return rtpParameters;
}
exports.getSendingRtpParameters = getSendingRtpParameters;
/**
* Generate RTP parameters of the given kind suitable for the remote SDP answer.
*/
function getSendingRemoteRtpParameters(kind, extendedRtpCapabilities) {
const rtpParameters = {
mid: undefined,
codecs: [],
headerExtensions: [],
encodings: [],
rtcp: {}
};
for (const extendedCodec of extendedRtpCapabilities.codecs) {
if (extendedCodec.kind !== kind) {
continue;
}
const codec = {
mimeType: extendedCodec.mimeType,
payloadType: extendedCodec.localPayloadType,
clockRate: extendedCodec.clockRate,
channels: extendedCodec.channels,
parameters: extendedCodec.remoteParameters,
rtcpFeedback: extendedCodec.rtcpFeedback
};
rtpParameters.codecs.push(codec);
// Add RTX codec.
if (extendedCodec.localRtxPayloadType) {
const rtxCodec = {
mimeType: `${extendedCodec.kind}/rtx`,
payloadType: extendedCodec.localRtxPayloadType,
clockRate: extendedCodec.clockRate,
parameters: {
apt: extendedCodec.localPayloadType
},
rtcpFeedback: []
};
rtpParameters.codecs.push(rtxCodec);
}
}
for (const extendedExtension of extendedRtpCapabilities.headerExtensions) {
// Ignore RTP extensions of a different kind and those not valid for sending.
if ((extendedExtension.kind && extendedExtension.kind !== kind) ||
(extendedExtension.direction !== 'sendrecv' &&
extendedExtension.direction !== 'sendonly')) {
continue;
}
const ext = {
uri: extendedExtension.uri,
id: extendedExtension.sendId,
encrypt: extendedExtension.encrypt,
parameters: {}
};
rtpParameters.headerExtensions.push(ext);
}
// Reduce codecs' RTCP feedback. Use Transport-CC if available, REMB otherwise.
if (rtpParameters.headerExtensions.some((ext) => (ext.uri === 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01'))) {
for (const codec of rtpParameters.codecs) {
codec.rtcpFeedback = (codec.rtcpFeedback || [])
.filter((fb) => fb.type !== 'goog-remb');
}
}
else if (rtpParameters.headerExtensions.some((ext) => (ext.uri === 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time'))) {
for (const codec of rtpParameters.codecs) {
codec.rtcpFeedback = (codec.rtcpFeedback || [])
.filter((fb) => fb.type !== 'transport-cc');
}
}
else {
for (const codec of rtpParameters.codecs) {
codec.rtcpFeedback = (codec.rtcpFeedback || [])
.filter((fb) => (fb.type !== 'transport-cc' &&
fb.type !== 'goog-remb'));
}
}
return rtpParameters;
}
exports.getSendingRemoteRtpParameters = getSendingRemoteRtpParameters;
/**
* Reduce given codecs by returning an array of codecs "compatible" with the
* given capability codec. If no capability codec is given, take the first
* one(s).
*
* Given codecs must be generated by ortc.getSendingRtpParameters() or
* ortc.getSendingRemoteRtpParameters().
*
* The returned array of codecs also include a RTX codec if available.
*/
function reduceCodecs(codecs, capCodec) {
const filteredCodecs = [];
// If no capability codec is given, take the first one (and RTX).
if (!capCodec) {
filteredCodecs.push(codecs[0]);
if (isRtxCodec(codecs[1])) {
filteredCodecs.push(codecs[1]);
}
}
// Otherwise look for a compatible set of codecs.
else {
for (let idx = 0; idx < codecs.length; ++idx) {
if (matchCodecs(codecs[idx], capCodec, { strict: true })) {
filteredCodecs.push(codecs[idx]);
if (isRtxCodec(codecs[idx + 1])) {
filteredCodecs.push(codecs[idx + 1]);
}
break;
}
}
if (filteredCodecs.length === 0) {
throw new TypeError('no matching codec found');
}
}
return filteredCodecs;
}
exports.reduceCodecs = reduceCodecs;
/**
* Create RTP parameters for a Consumer for the RTP probator.
*/
function generateProbatorRtpParameters(videoRtpParameters) {
// Clone given reference video RTP parameters.
videoRtpParameters = utils.clone(videoRtpParameters, {});
// This may throw.
validateRtpParameters(videoRtpParameters);
const rtpParameters = {
mid: RTP_PROBATOR_MID,
codecs: [],
headerExtensions: [],
encodings: [{ ssrc: RTP_PROBATOR_SSRC }],
rtcp: { cname: 'probator' }
};
rtpParameters.codecs.push(videoRtpParameters.codecs[0]);
rtpParameters.codecs[0].payloadType = RTP_PROBATOR_CODEC_PAYLOAD_TYPE;
rtpParameters.headerExtensions = videoRtpParameters.headerExtensions;
return rtpParameters;
}
exports.generateProbatorRtpParameters = generateProbatorRtpParameters;
/**
* Whether media can be sent based on the given RTP capabilities.
*/
function canSend(kind, extendedRtpCapabilities) {
return extendedRtpCapabilities.codecs.
some((codec) => codec.kind === kind);
}
exports.canSend = canSend;
/**
* Whether the given RTP parameters can be received with the given RTP
* capabilities.
*/
function canReceive(rtpParameters, extendedRtpCapabilities) {
// This may throw.
validateRtpParameters(rtpParameters);
if (rtpParameters.codecs.length === 0) {
return false;
}
const firstMediaCodec = rtpParameters.codecs[0];
return extendedRtpCapabilities.codecs
.some((codec) => codec.remotePayloadType === firstMediaCodec.payloadType);
}
exports.canReceive = canReceive;
function isRtxCodec(codec) {
if (!codec) {
return false;
}
return /.+\/rtx$/i.test(codec.mimeType);
}
function matchCodecs(aCodec, bCodec, { strict = false, modify = false } = {}) {
var _a, _b, _c, _d;
const aMimeType = aCodec.mimeType.toLowerCase();
const bMimeType = bCodec.mimeType.toLowerCase();
if (aMimeType !== bMimeType) {
return false;
}
if (aCodec.clockRate !== bCodec.clockRate) {
return false;
}
if (aCodec.channels !== bCodec.channels) {
return false;
}
// Per codec special checks.
switch (aMimeType) {
case 'video/h264':
{
const aPacketizationMode = (_a = aCodec.parameters['packetization-mode']) !== null && _a !== void 0 ? _a : 0;
const bPacketizationMode = (_b = bCodec.parameters['packetization-mode']) !== null && _b !== void 0 ? _b : 0;
const aAsymmetry = (_c = aCodec.parameters['level-asymmetry-allowed']) !== null && _c !== void 0 ? _c : 1;
const bAsymmetry = (_d = bCodec.parameters['level-asymmetry-allowed']) !== null && _d !== void 0 ? _d : 1;
if (aPacketizationMode !== bPacketizationMode) {
return false;
}
if (aAsymmetry !== bAsymmetry) {
return false;
}
// If strict matching check profile-level-id.
if (strict) {
if (!h264.isSameProfile(aCodec.parameters, bCodec.parameters)) {
return false;
}
let selectedProfileLevelId;
try {
selectedProfileLevelId =
h264.generateProfileLevelIdForAnswer(aCodec.parameters, bCodec.parameters);
}
catch (error) {
return false;
}
if (modify) {
if (selectedProfileLevelId) {
aCodec.parameters['profile-level-id'] = selectedProfileLevelId;
bCodec.parameters['profile-level-id'] = selectedProfileLevelId;
}
else {
delete aCodec.parameters['profile-level-id'];
delete bCodec.parameters['profile-level-id'];
}
}
}
break;
}
case 'video/vp9':
{
// If strict matching check profile-id.
if (strict) {
const aProfileId = aCodec.parameters['profile-id'] || 0;
const bProfileId = bCodec.parameters['profile-id'] || 0;
if (aProfileId !== bProfileId) {
return false;
}
}
break;
}
}
return true;
}
function matchHeaderExtensions(aExt, bExt) {
if (aExt.kind && bExt.kind && aExt.kind !== bExt.kind) {
return false;
}
if (aExt.uri !== bExt.uri) {
return false;
}
return true;
}
function reduceRtcpFeedback(codecA, codecB) {
const reducedRtcpFeedback = [];
for (const aFb of codecA.rtcpFeedback || []) {
const matchingBFb = (codecB.rtcpFeedback || [])
.find((bFb) => (bFb.type === aFb.type &&
(bFb.parameter === aFb.parameter || (!bFb.parameter && !aFb.parameter))));
if (matchingBFb) {
reducedRtcpFeedback.push(matchingBFb);
}
}
return reducedRtcpFeedback;
}