mediasoup
Version:
Cutting Edge WebRTC Video Conferencing
909 lines (908 loc) • 35.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateRtpCapabilities = validateRtpCapabilities;
exports.validateRtpParameters = validateRtpParameters;
exports.validateSctpStreamParameters = validateSctpStreamParameters;
exports.generateRouterRtpCapabilities = generateRouterRtpCapabilities;
exports.getProducerRtpParametersMapping = getProducerRtpParametersMapping;
exports.getConsumableRtpParameters = getConsumableRtpParameters;
exports.canConsume = canConsume;
exports.getConsumerRtpParameters = getConsumerRtpParameters;
exports.getPipeConsumerRtpParameters = getPipeConsumerRtpParameters;
exports.serializeRtpMapping = serializeRtpMapping;
const h264 = require("h264-profile-level-id");
const supportedRtpCapabilities_1 = require("./supportedRtpCapabilities");
const scalabilityModes_1 = require("./scalabilityModes");
const utils = require("./utils");
const errors_1 = require("./errors");
const FbsRtpParameters = require("./fbs/rtp-parameters");
const DynamicPayloadTypes = [
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114,
115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 96, 97, 98,
99,
];
/**
* 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);
}
}
/**
* 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);
}
/**
* 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;
}
}
/**
* Generate RTP capabilities for the Router based on the given media codecs and
* mediasoup supported RTP capabilities.
*/
function generateRouterRtpCapabilities(mediaCodecs = []) {
// Normalize supported RTP capabilities.
validateRtpCapabilities(supportedRtpCapabilities_1.supportedRtpCapabilities);
if (!Array.isArray(mediaCodecs)) {
throw new TypeError('mediaCodecs must be an Array');
}
const clonedSupportedRtpCapabilities = utils.clone(supportedRtpCapabilities_1.supportedRtpCapabilities);
const dynamicPayloadTypes = utils.clone(DynamicPayloadTypes);
const caps = {
codecs: [],
headerExtensions: clonedSupportedRtpCapabilities.headerExtensions,
};
for (const mediaCodec of mediaCodecs) {
// This may throw.
validateRtpCodecCapability(mediaCodec);
const matchedSupportedCodec = clonedSupportedRtpCapabilities.codecs.find(supportedCodec => matchCodecs(mediaCodec, supportedCodec, { strict: false }));
if (!matchedSupportedCodec) {
throw new errors_1.UnsupportedError(`media codec not supported [mimeType:${mediaCodec.mimeType}]`);
}
// Clone the supported codec.
const codec = utils.clone(matchedSupportedCodec);
// If the given media codec has preferredPayloadType, keep it.
if (typeof mediaCodec.preferredPayloadType === 'number') {
codec.preferredPayloadType = mediaCodec.preferredPayloadType;
// Also remove the pt from the list of available dynamic values.
const idx = dynamicPayloadTypes.indexOf(codec.preferredPayloadType);
if (idx > -1) {
dynamicPayloadTypes.splice(idx, 1);
}
}
// Otherwise if the supported codec has preferredPayloadType, use it.
else if (typeof codec.preferredPayloadType === 'number') {
// No need to remove it from the list since it's not a dynamic value.
}
// Otherwise choose a dynamic one.
else {
// Take the first available pt and remove it from the list.
const pt = dynamicPayloadTypes.shift();
if (!pt) {
throw new Error('cannot allocate more dynamic codec payload types');
}
codec.preferredPayloadType = pt;
}
// Ensure there is not duplicated preferredPayloadType values.
if (caps.codecs.some(c => c.preferredPayloadType === codec.preferredPayloadType)) {
throw new TypeError('duplicated codec.preferredPayloadType');
}
// Merge the media codec parameters.
codec.parameters = { ...codec.parameters, ...mediaCodec.parameters };
// Append to the codec list.
caps.codecs.push(codec);
// Add a RTX video codec if video.
if (codec.kind === 'video') {
// Take the first available pt and remove it from the list.
const pt = dynamicPayloadTypes.shift();
if (!pt) {
throw new Error('cannot allocate more dynamic codec payload types');
}
const rtxCodec = {
kind: codec.kind,
mimeType: `${codec.kind}/rtx`,
preferredPayloadType: pt,
clockRate: codec.clockRate,
parameters: {
apt: codec.preferredPayloadType,
},
rtcpFeedback: [],
};
// Append to the codec list.
caps.codecs.push(rtxCodec);
}
}
return caps;
}
/**
* Get a mapping of codec payloads and encodings of the given Producer RTP
* parameters as values expected by the Router.
*
* It may throw if invalid or non supported RTP parameters are given.
*/
function getProducerRtpParametersMapping(params, caps) {
const rtpMapping = {
codecs: [],
encodings: [],
};
// Match parameters media codecs to capabilities media codecs.
const codecToCapCodec = new Map();
for (const codec of params.codecs) {
if (isRtxCodec(codec)) {
continue;
}
// Search for the same media codec in capabilities.
const matchedCapCodec = caps.codecs.find(capCodec => matchCodecs(codec, capCodec, { strict: true, modify: true }));
if (!matchedCapCodec) {
throw new errors_1.UnsupportedError(`unsupported codec [mimeType:${codec.mimeType}, payloadType:${codec.payloadType}]`);
}
codecToCapCodec.set(codec, matchedCapCodec);
}
// Match parameters RTX codecs to capabilities RTX codecs.
for (const codec of params.codecs) {
if (!isRtxCodec(codec)) {
continue;
}
// Search for the associated media codec.
const associatedMediaCodec = params.codecs.find(mediaCodec => mediaCodec.payloadType === codec.parameters.apt);
if (!associatedMediaCodec) {
throw new TypeError(`missing media codec found for RTX PT ${codec.payloadType}`);
}
const capMediaCodec = codecToCapCodec.get(associatedMediaCodec);
// Ensure that the capabilities media codec has a RTX codec.
const associatedCapRtxCodec = caps.codecs.find(capCodec => isRtxCodec(capCodec) &&
capCodec.parameters.apt === capMediaCodec.preferredPayloadType);
if (!associatedCapRtxCodec) {
throw new errors_1.UnsupportedError(`no RTX codec for capability codec PT ${capMediaCodec.preferredPayloadType}`);
}
codecToCapCodec.set(codec, associatedCapRtxCodec);
}
// Generate codecs mapping.
for (const [codec, capCodec] of codecToCapCodec) {
rtpMapping.codecs.push({
payloadType: codec.payloadType,
mappedPayloadType: capCodec.preferredPayloadType,
});
}
// Generate encodings mapping.
let mappedSsrc = utils.generateRandomNumber();
for (const encoding of params.encodings) {
const mappedEncoding = {};
mappedEncoding.mappedSsrc = mappedSsrc++;
if (encoding.rid) {
mappedEncoding.rid = encoding.rid;
}
if (encoding.ssrc) {
mappedEncoding.ssrc = encoding.ssrc;
}
if (encoding.scalabilityMode) {
mappedEncoding.scalabilityMode = encoding.scalabilityMode;
}
rtpMapping.encodings.push(mappedEncoding);
}
return rtpMapping;
}
/**
* Generate RTP parameters to be internally used by Consumers given the RTP
* parameters of a Producer and the RTP capabilities of the Router.
*/
function getConsumableRtpParameters(kind, params, caps, rtpMapping) {
const consumableParams = {
codecs: [],
headerExtensions: [],
encodings: [],
rtcp: {},
};
for (const codec of params.codecs) {
if (isRtxCodec(codec)) {
continue;
}
const consumableCodecPt = rtpMapping.codecs.find(entry => entry.payloadType === codec.payloadType).mappedPayloadType;
const matchedCapCodec = caps.codecs.find(capCodec => capCodec.preferredPayloadType === consumableCodecPt);
const consumableCodec = {
mimeType: matchedCapCodec.mimeType,
payloadType: matchedCapCodec.preferredPayloadType,
clockRate: matchedCapCodec.clockRate,
channels: matchedCapCodec.channels,
parameters: codec.parameters, // Keep the Producer codec parameters.
rtcpFeedback: matchedCapCodec.rtcpFeedback,
};
consumableParams.codecs.push(consumableCodec);
const consumableCapRtxCodec = caps.codecs.find(capRtxCodec => isRtxCodec(capRtxCodec) &&
capRtxCodec.parameters.apt === consumableCodec.payloadType);
if (consumableCapRtxCodec) {
const consumableRtxCodec = {
mimeType: consumableCapRtxCodec.mimeType,
payloadType: consumableCapRtxCodec.preferredPayloadType,
clockRate: consumableCapRtxCodec.clockRate,
parameters: consumableCapRtxCodec.parameters,
rtcpFeedback: consumableCapRtxCodec.rtcpFeedback,
};
consumableParams.codecs.push(consumableRtxCodec);
}
}
for (const capExt of caps.headerExtensions) {
// Just take RTP header extension that can be used in Consumers.
if (capExt.kind !== kind ||
(capExt.direction !== 'sendrecv' && capExt.direction !== 'sendonly')) {
continue;
}
const consumableExt = {
uri: capExt.uri,
id: capExt.preferredId,
encrypt: capExt.preferredEncrypt,
parameters: {},
};
consumableParams.headerExtensions.push(consumableExt);
}
// Clone Producer encodings since we'll mangle them.
const consumableEncodings = utils.clone(params.encodings) ?? [];
for (let i = 0; i < consumableEncodings.length; ++i) {
const consumableEncoding = consumableEncodings[i];
const { mappedSsrc } = rtpMapping.encodings[i];
// Remove useless fields.
delete consumableEncoding.rid;
delete consumableEncoding.rtx;
delete consumableEncoding.codecPayloadType;
// Set the mapped ssrc.
consumableEncoding.ssrc = mappedSsrc;
consumableParams.encodings.push(consumableEncoding);
}
consumableParams.rtcp = {
cname: params.rtcp.cname,
reducedSize: true,
};
return consumableParams;
}
/**
* Check whether the given RTP capabilities can consume the given Producer.
*/
function canConsume(consumableParams, caps) {
// This may throw.
validateRtpCapabilities(caps);
const matchingCodecs = [];
for (const codec of consumableParams.codecs) {
const matchedCapCodec = caps.codecs.find(capCodec => matchCodecs(capCodec, codec, { strict: true }));
if (!matchedCapCodec) {
continue;
}
matchingCodecs.push(codec);
}
// Ensure there is at least one media codec.
if (matchingCodecs.length === 0 || isRtxCodec(matchingCodecs[0])) {
return false;
}
return true;
}
/**
* Generate RTP parameters for a specific Consumer.
*
* It reduces encodings to just one and takes into account given RTP
* capabilities to reduce codecs, codecs' RTCP feedback and header extensions,
* and also enables or disables RTX.
*/
function getConsumerRtpParameters({ consumableRtpParameters, remoteRtpCapabilities, pipe, enableRtx, }) {
const consumerParams = {
codecs: [],
headerExtensions: [],
encodings: [],
rtcp: consumableRtpParameters.rtcp,
};
for (const capCodec of remoteRtpCapabilities.codecs) {
validateRtpCodecCapability(capCodec);
}
const consumableCodecs = utils.clone(consumableRtpParameters.codecs) ?? [];
let rtxSupported = false;
for (const codec of consumableCodecs) {
if (!enableRtx && isRtxCodec(codec)) {
continue;
}
const matchedCapCodec = remoteRtpCapabilities.codecs.find(capCodec => matchCodecs(capCodec, codec, { strict: true }));
if (!matchedCapCodec) {
continue;
}
codec.rtcpFeedback = matchedCapCodec.rtcpFeedback.filter(fb => enableRtx || fb.type !== 'nack' || fb.parameter);
consumerParams.codecs.push(codec);
}
// Must sanitize the list of matched codecs by removing useless RTX codecs.
for (let idx = consumerParams.codecs.length - 1; idx >= 0; --idx) {
const codec = consumerParams.codecs[idx];
if (isRtxCodec(codec)) {
// Search for the associated media codec.
const associatedMediaCodec = consumerParams.codecs.find(mediaCodec => mediaCodec.payloadType === codec.parameters.apt);
if (associatedMediaCodec) {
rtxSupported = true;
}
else {
consumerParams.codecs.splice(idx, 1);
}
}
}
// Ensure there is at least one media codec.
if (consumerParams.codecs.length === 0 ||
isRtxCodec(consumerParams.codecs[0])) {
throw new errors_1.UnsupportedError('no compatible media codecs');
}
consumerParams.headerExtensions =
consumableRtpParameters.headerExtensions.filter(ext => remoteRtpCapabilities.headerExtensions.some(capExt => capExt.preferredId === ext.id && capExt.uri === ext.uri));
// Reduce codecs' RTCP feedback. Use Transport-CC if available, REMB otherwise.
if (consumerParams.headerExtensions.some(ext => ext.uri ===
'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01')) {
for (const codec of consumerParams.codecs) {
codec.rtcpFeedback = codec.rtcpFeedback.filter(fb => fb.type !== 'goog-remb');
}
}
else if (consumerParams.headerExtensions.some(ext => ext.uri === 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time')) {
for (const codec of consumerParams.codecs) {
codec.rtcpFeedback = codec.rtcpFeedback.filter(fb => fb.type !== 'transport-cc');
}
}
else {
for (const codec of consumerParams.codecs) {
codec.rtcpFeedback = codec.rtcpFeedback.filter(fb => fb.type !== 'transport-cc' && fb.type !== 'goog-remb');
}
}
if (!pipe) {
const consumerEncoding = {
ssrc: utils.generateRandomNumber(),
};
if (rtxSupported) {
consumerEncoding.rtx = { ssrc: consumerEncoding.ssrc + 1 };
}
// If any of the consumableRtpParameters.encodings has scalabilityMode,
// process it (assume all encodings have the same value).
const encodingWithScalabilityMode = consumableRtpParameters.encodings.find(encoding => encoding.scalabilityMode);
let scalabilityMode = encodingWithScalabilityMode
? encodingWithScalabilityMode.scalabilityMode
: undefined;
// If there is simulast, mangle spatial layers in scalabilityMode.
if (consumableRtpParameters.encodings.length > 1) {
const { temporalLayers } = (0, scalabilityModes_1.parse)(scalabilityMode);
scalabilityMode = `L${consumableRtpParameters.encodings.length}T${temporalLayers}`;
}
if (scalabilityMode) {
consumerEncoding.scalabilityMode = scalabilityMode;
}
// Use the maximum maxBitrate in any encoding and honor it in the Consumer's
// encoding.
const maxEncodingMaxBitrate = consumableRtpParameters.encodings.reduce((maxBitrate, encoding) => encoding.maxBitrate && encoding.maxBitrate > maxBitrate
? encoding.maxBitrate
: maxBitrate, 0);
if (maxEncodingMaxBitrate) {
consumerEncoding.maxBitrate = maxEncodingMaxBitrate;
}
// Set a single encoding for the Consumer.
consumerParams.encodings.push(consumerEncoding);
}
else {
const consumableEncodings = utils.clone(consumableRtpParameters.encodings) ?? [];
const baseSsrc = utils.generateRandomNumber();
const baseRtxSsrc = utils.generateRandomNumber();
for (let i = 0; i < consumableEncodings.length; ++i) {
const encoding = consumableEncodings[i];
encoding.ssrc = baseSsrc + i;
if (rtxSupported) {
encoding.rtx = { ssrc: baseRtxSsrc + i };
}
else {
delete encoding.rtx;
}
consumerParams.encodings.push(encoding);
}
}
return consumerParams;
}
/**
* Generate RTP parameters for a pipe Consumer.
*
* It keeps all original consumable encodings and removes support for BWE. If
* enableRtx is false, it also removes RTX and NACK support.
*/
function getPipeConsumerRtpParameters({ consumableRtpParameters, enableRtx, }) {
const consumerParams = {
codecs: [],
headerExtensions: [],
encodings: [],
rtcp: consumableRtpParameters.rtcp,
};
const consumableCodecs = utils.clone(consumableRtpParameters.codecs) ?? [];
for (const codec of consumableCodecs) {
if (!enableRtx && isRtxCodec(codec)) {
continue;
}
codec.rtcpFeedback = codec.rtcpFeedback.filter(fb => (fb.type === 'nack' && fb.parameter === 'pli') ||
(fb.type === 'ccm' && fb.parameter === 'fir') ||
(enableRtx && fb.type === 'nack' && !fb.parameter));
consumerParams.codecs.push(codec);
}
// Reduce RTP extensions by disabling transport MID and BWE related ones.
consumerParams.headerExtensions =
consumableRtpParameters.headerExtensions.filter(ext => ext.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid' &&
ext.uri !==
'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time' &&
ext.uri !==
'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01');
const consumableEncodings = utils.clone(consumableRtpParameters.encodings) ?? [];
const baseSsrc = utils.generateRandomNumber();
const baseRtxSsrc = utils.generateRandomNumber();
for (let i = 0; i < consumableEncodings.length; ++i) {
const encoding = consumableEncodings[i];
encoding.ssrc = baseSsrc + i;
if (enableRtx) {
encoding.rtx = { ssrc: baseRtxSsrc + i };
}
else {
delete encoding.rtx;
}
consumerParams.encodings.push(encoding);
}
return consumerParams;
}
function isRtxCodec(codec) {
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 'audio/multiopus': {
const aNumStreams = aCodec.parameters['num_streams'];
const bNumStreams = bCodec.parameters['num_streams'];
if (aNumStreams !== bNumStreams) {
return false;
}
const aCoupledStreams = aCodec.parameters['coupled_streams'];
const bCoupledStreams = bCodec.parameters['coupled_streams'];
if (aCoupledStreams !== bCoupledStreams) {
return false;
}
break;
}
case 'video/h264':
case 'video/h264-svc': {
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;
}
else {
delete aCodec.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 serializeRtpMapping(builder, rtpMapping) {
const codecs = [];
for (const codec of rtpMapping.codecs) {
codecs.push(FbsRtpParameters.CodecMapping.createCodecMapping(builder, codec.payloadType, codec.mappedPayloadType));
}
const codecsOffset = FbsRtpParameters.RtpMapping.createCodecsVector(builder, codecs);
const encodings = [];
for (const encoding of rtpMapping.encodings) {
encodings.push(FbsRtpParameters.EncodingMapping.createEncodingMapping(builder, builder.createString(encoding.rid), encoding.ssrc ?? null, builder.createString(encoding.scalabilityMode), encoding.mappedSsrc));
}
const encodingsOffset = FbsRtpParameters.RtpMapping.createEncodingsVector(builder, encodings);
return FbsRtpParameters.RtpMapping.createRtpMapping(builder, codecsOffset, encodingsOffset);
}
/**
* 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);
}
}
/**
* 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 = '';
}
}
/**
* 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');
}
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 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);
}
}
/**
* 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 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');
}
}
/**
* 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;
}
}