mediasoup-client
Version:
mediasoup client side TypeScript library
925 lines (924 loc) • 34.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateAndNormalizeRtpCapabilities = validateAndNormalizeRtpCapabilities;
exports.validateAndNormalizeRtpParameters = validateAndNormalizeRtpParameters;
exports.validateAndNormalizeSctpStreamParameters = validateAndNormalizeSctpStreamParameters;
exports.validateSctpCapabilities = validateSctpCapabilities;
exports.getExtendedRtpCapabilities = getExtendedRtpCapabilities;
exports.getRecvRtpCapabilities = getRecvRtpCapabilities;
exports.getSendRtpCapabilities = getSendRtpCapabilities;
exports.getSendingRtpParameters = getSendingRtpParameters;
exports.getSendingRemoteRtpParameters = getSendingRemoteRtpParameters;
exports.reduceCodecs = reduceCodecs;
exports.generateProbatorRtpParameters = generateProbatorRtpParameters;
exports.canSend = canSend;
exports.canReceive = canReceive;
const h264 = require("h264-profile-level-id");
const utils = 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 validateAndNormalizeRtpCapabilities(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) {
validateAndNormalizeRtpCodecCapability(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) {
validateAndNormalizeRtpHeaderExtension(ext);
}
}
/**
* Validates RtpParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateAndNormalizeRtpParameters(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) {
validateAndNormalizeRtpCodecParameters(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) {
validateAndNormalizeRtpEncodingParameters(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 = {};
}
validateAndNormalizeRtcpParameters(params.rtcp);
}
/**
* Validates SctpStreamParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateAndNormalizeSctpStreamParameters(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;
}
// 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');
}
}
/**
* Validates SctpCapabilities.
* 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);
}
/**
* Generate extended RTP capabilities for sending and receiving.
*
* Resulting codecs keep order preferred by local or remote capabilities
* depending on `preferLocalCodecsOrder`.
*/
function getExtendedRtpCapabilities(localCaps, remoteCaps, preferLocalCodecsOrder) {
const extendedRtpCapabilities = {
codecs: [],
headerExtensions: [],
};
// Match media codecs and keep the order preferred by local capabilities.
if (preferLocalCodecsOrder) {
for (const localCodec of localCaps.codecs ?? []) {
if (isRtxCodec(localCodec)) {
continue;
}
const matchingRemoteCodec = (remoteCaps.codecs ?? []).find((remoteCodec) => matchCodecs(remoteCodec, localCodec, { strict: true, modify: true }));
if (!matchingRemoteCodec) {
continue;
}
const extendedCodec = {
kind: localCodec.kind,
mimeType: localCodec.mimeType,
clockRate: localCodec.clockRate,
channels: localCodec.channels,
localPayloadType: localCodec.preferredPayloadType,
localRtxPayloadType: undefined,
remotePayloadType: matchingRemoteCodec.preferredPayloadType,
remoteRtxPayloadType: undefined,
localParameters: localCodec.parameters ?? {},
remoteParameters: matchingRemoteCodec.parameters ?? {},
rtcpFeedback: reduceRtcpFeedback(localCodec, matchingRemoteCodec),
};
extendedRtpCapabilities.codecs.push(extendedCodec);
}
}
// Match media codecs and keep the order preferred by remote capabilities.
else {
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 = {
kind: matchingLocalCodec.kind,
mimeType: matchingLocalCodec.mimeType,
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 ?? false,
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;
}
/**
* Generate RTP capabilities for receiving media based on the given extended
* RTP capabilities.
*/
function getRecvRtpCapabilities(extendedRtpCapabilities) {
return getRtpCapabilities({ direction: 'recvonly', extendedRtpCapabilities });
}
/**
* Generate RTP capabilities for sending media based on the given extended
* RTP capabilities.
*/
function getSendRtpCapabilities(extendedRtpCapabilities) {
return getRtpCapabilities({ direction: 'sendonly', extendedRtpCapabilities });
}
/**
* 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;
}
/**
* 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;
}
/**
* 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;
}
/**
* 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.
validateAndNormalizeRtpParameters(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;
}
/**
* Whether media can be sent based on the given RTP capabilities.
*/
function canSend(kind, rtpCapabilities) {
return (rtpCapabilities.codecs ?? []).some(codec => codec.kind === kind);
}
/**
* Whether the given RTP parameters can be received with the given RTP
* capabilities.
*/
function canReceive(rtpParameters, rtpCapabilities) {
// This may throw.
validateAndNormalizeRtpParameters(rtpParameters);
if (rtpParameters.codecs.length === 0) {
return false;
}
const firstMediaCodec = rtpParameters.codecs[0];
return (rtpCapabilities.codecs ?? []).some(codec => codec.preferredPayloadType === firstMediaCodec.payloadType);
}
/**
* Validates RtpCodecCapability. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateAndNormalizeRtpCodecCapability(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 mandatory.
if (typeof codec.preferredPayloadType !== 'number') {
throw new TypeError('missing 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) {
validateAndNormalizeRtcpFeedback(fb);
}
}
/**
* Validates RtcpFeedback. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateAndNormalizeRtcpFeedback(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 = '';
}
}
/**
* Validates RtpHeaderExtension. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateAndNormalizeRtpHeaderExtension(ext) {
if (typeof ext !== 'object') {
throw new TypeError('ext is not an object');
}
// kind is mandatory.
if (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';
}
}
/**
* Validates RtpCodecParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateAndNormalizeRtpCodecParameters(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) {
validateAndNormalizeRtcpFeedback(fb);
}
}
/**
* 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');
}
}
}
/**
* Validates RtpEncodingParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateAndNormalizeRtpEncodingParameters(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');
}
}
/**
* Validates RtcpParameters. It may modify given data by adding missing
* fields with default values.
* It throws if invalid.
*/
function validateAndNormalizeRtcpParameters(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;
}
}
/**
* Validates NumSctpStreams.
* 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');
}
}
/**
* Generate RTP capabilities for sending or receiving media based on the given
* extended RTP capabilities.
*/
function getRtpCapabilities({ direction, extendedRtpCapabilities, }) {
const rtpCapabilities = {
codecs: [],
headerExtensions: [],
};
for (const extendedCodec of extendedRtpCapabilities.codecs) {
const codec = {
kind: extendedCodec.kind,
mimeType: extendedCodec.mimeType,
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 = {
kind: extendedCodec.kind,
mimeType: `${extendedCodec.kind}/rtx`,
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 the given direction.
if (extendedExtension.direction !== 'sendrecv' &&
extendedExtension.direction !== direction) {
continue;
}
const ext = {
kind: extendedExtension.kind,
uri: extendedExtension.uri,
preferredId: extendedExtension.recvId,
preferredEncrypt: extendedExtension.encrypt ?? false,
direction: extendedExtension.direction,
};
rtpCapabilities.headerExtensions.push(ext);
}
return rtpCapabilities;
}
function isRtxCodec(codec) {
if (!codec) {
return false;
}
return /.+\/rtx$/i.test(codec.mimeType);
}
function matchCodecs(aCodec, bCodec, { strict = false, modify = false } = {}) {
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': {
if (strict) {
const aPacketizationMode = aCodec.parameters['packetization-mode'] ?? 0;
const bPacketizationMode = bCodec.parameters['packetization-mode'] ?? 0;
if (aPacketizationMode !== bPacketizationMode) {
return false;
}
if (!h264.isSameProfile(aCodec.parameters, bCodec.parameters)) {
return false;
}
let selectedProfileLevelId;
try {
selectedProfileLevelId = h264.generateProfileLevelIdStringForAnswer(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) {
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;
}