UNPKG

msc-node

Version:

mediasoup client side Node.js library

959 lines (958 loc) 36.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; 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; }