UNPKG

mediasoup-client

Version:

mediasoup client side TypeScript library

925 lines (924 loc) 34.2 kB
"use strict"; 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; }