UNPKG

twilio-video

Version:

Twilio Video JavaScript Library

537 lines 23.9 kB
'use strict'; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var _a = require('../'), difference = _a.difference, flatMap = _a.flatMap; var setSimulcastInMediaSection = require('./simulcast'); var ptToFixedBitrateAudioCodecName = { 0: 'PCMU', 8: 'PCMA' }; /** * A payload type * @typedef {number} PT */ /** * An {@link AudioCodec} or {@link VideoCodec} * @typedef {AudioCodec|VideoCodec} Codec */ /** * Create a Codec Map for the given m= section. * @param {string} section - The given m= section * @returns {Map<Codec, Array<PT>>} */ function createCodecMapForMediaSection(section) { return Array.from(createPtToCodecName(section)).reduce(function (codecMap, pair) { var pt = pair[0]; var codecName = pair[1]; var pts = codecMap.get(codecName) || []; return codecMap.set(codecName, pts.concat(pt)); }, new Map()); } /** * Create a Map of MIDs to m= sections for the given SDP. * @param {string} sdp * @returns {Map<string, string>} */ function createMidToMediaSectionMap(sdp) { return getMediaSections(sdp).reduce(function (midsToMediaSections, mediaSection) { var mid = getMidForMediaSection(mediaSection); return mid ? midsToMediaSections.set(mid, mediaSection) : midsToMediaSections; }, new Map()); } /** * Create a Map from PTs to codec names for the given m= section. * @param {string} mediaSection - The given m= section. * @returns {Map<PT, Codec>} ptToCodecName */ function createPtToCodecName(mediaSection) { return getPayloadTypesInMediaSection(mediaSection).reduce(function (ptToCodecName, pt) { var rtpmapPattern = new RegExp("a=rtpmap:" + pt + " ([^/]+)"); var matches = mediaSection.match(rtpmapPattern); var codecName = matches ? matches[1].toLowerCase() : ptToFixedBitrateAudioCodecName[pt] ? ptToFixedBitrateAudioCodecName[pt].toLowerCase() : ''; return ptToCodecName.set(pt, codecName); }, new Map()); } /** * Get the associated fmtp attributes for the given Payload Type in an m= section. * @param {PT} pt * @param {string} mediaSection * @returns {?object} */ function getFmtpAttributesForPt(pt, mediaSection) { // In "a=fmtp:<pt> <name>=<value>[;<name>=<value>]*", the regex matches the codec // profile parameters expressed as name/value pairs separated by ";". var fmtpRegex = new RegExp("^a=fmtp:" + pt + " (.+)$", 'm'); var matches = mediaSection.match(fmtpRegex); return matches && matches[1].split(';').reduce(function (attrs, nvPair) { var _a = __read(nvPair.split('='), 2), name = _a[0], value = _a[1]; attrs[name] = isNaN(value) ? value : parseInt(value, 10); return attrs; }, {}); } /** * Get the MID for the given m= section. * @param {string} mediaSection * @return {?string} */ function getMidForMediaSection(mediaSection) { // In "a=mid:<mid>", the regex matches <mid>. var midMatches = mediaSection.match(/^a=mid:(.+)$/m); return midMatches && midMatches[1]; } /** * Get the m= sections of a particular kind and direction from an sdp. * @param {string} sdp - SDP string * @param {string} [kind] - Pattern for matching kind * @param {string} [direction] - Pattern for matching direction * @returns {Array<string>} mediaSections */ function getMediaSections(sdp, kind, direction) { return sdp.replace(/\r\n\r\n$/, '\r\n').split('\r\nm=').slice(1).map(function (mediaSection) { return "m=" + mediaSection; }).filter(function (mediaSection) { var kindPattern = new RegExp("m=" + (kind || '.*'), 'gm'); var directionPattern = new RegExp("a=" + (direction || '.*'), 'gm'); return kindPattern.test(mediaSection) && directionPattern.test(mediaSection); }); } /** * Get the Codec Payload Types present in the first line of the given m= section * @param {string} section - The m= section * @returns {Array<PT>} Payload Types */ function getPayloadTypesInMediaSection(section) { var mLine = section.split('\r\n')[0]; // In "m=<kind> <port> <proto> <payload_type_1> <payload_type_2> ... <payload_type_n>", // the regex matches <port> and the Payload Types. var matches = mLine.match(/([0-9]+)/g); // This should not happen, but in case there are no Payload Types in // the m= line, return an empty array. if (!matches) { return []; } // Since only the Payload Types are needed, we discard the <port>. return matches.slice(1).map(function (match) { return parseInt(match, 10); }); } /** * Create the reordered Codec Payload Types based on the preferred Codec Names. * @param {Map<Codec, Array<PT>>} codecMap - Codec Map * @param {Array<AudioCodecSettings|VideoCodecSettings>} preferredCodecs - Preferred Codecs * @returns {Array<PT>} Reordered Payload Types */ function getReorderedPayloadTypes(codecMap, preferredCodecs) { preferredCodecs = preferredCodecs.map(function (_a) { var codec = _a.codec; return codec.toLowerCase(); }); var preferredPayloadTypes = flatMap(preferredCodecs, function (codecName) { return codecMap.get(codecName) || []; }); var remainingCodecs = difference(Array.from(codecMap.keys()), preferredCodecs); var remainingPayloadTypes = flatMap(remainingCodecs, function (codecName) { return codecMap.get(codecName); }); return preferredPayloadTypes.concat(remainingPayloadTypes); } /** * Set the given Codec Payload Types in the first line of the given m= section. * @param {Array<PT>} payloadTypes - Payload Types * @param {string} section - Given m= section * @returns {string} - Updated m= section */ function setPayloadTypesInMediaSection(payloadTypes, section) { var lines = section.split('\r\n'); var mLine = lines[0]; var otherLines = lines.slice(1); mLine = mLine.replace(/([0-9]+\s?)+$/, payloadTypes.join(' ')); return [mLine].concat(otherLines).join('\r\n'); } /** * Return a new SDP string with the re-ordered codec preferences. * @param {string} sdp * @param {Array<AudioCodec>} preferredAudioCodecs - If empty, the existing order * of audio codecs is preserved * @param {Array<VideoCodecSettings>} preferredVideoCodecs - If empty, the * existing order of video codecs is preserved * @returns {string} Updated SDP string */ function setCodecPreferences(sdp, preferredAudioCodecs, preferredVideoCodecs) { var mediaSections = getMediaSections(sdp); var session = sdp.split('\r\nm=')[0]; return [session].concat(mediaSections.map(function (section) { // Codec preferences should not be applied to m=application sections. if (!/^m=(audio|video)/.test(section)) { return section; } var kind = section.match(/^m=(audio|video)/)[1]; var codecMap = createCodecMapForMediaSection(section); var preferredCodecs = kind === 'audio' ? preferredAudioCodecs : preferredVideoCodecs; var payloadTypes = getReorderedPayloadTypes(codecMap, preferredCodecs); var newSection = setPayloadTypesInMediaSection(payloadTypes, section); var pcmaPayloadTypes = codecMap.get('pcma') || []; var pcmuPayloadTypes = codecMap.get('pcmu') || []; var fixedBitratePayloadTypes = kind === 'audio' ? new Set(pcmaPayloadTypes.concat(pcmuPayloadTypes)) : new Set(); return fixedBitratePayloadTypes.has(payloadTypes[0]) ? newSection.replace(/\r\nb=(AS|TIAS):([0-9]+)/g, '') : newSection; })).join('\r\n'); } /** * Return a new SDP string with simulcast settings. * @param {string} sdp * @param {Map<Track.ID, TrackAttributes>} trackIdsToAttributes * @returns {string} Updated SDP string */ function setSimulcast(sdp, trackIdsToAttributes) { var mediaSections = getMediaSections(sdp); var session = sdp.split('\r\nm=')[0]; return [session].concat(mediaSections.map(function (section) { section = section.replace(/\r\n$/, ''); if (!/^m=video/.test(section)) { return section; } var codecMap = createCodecMapForMediaSection(section); var payloadTypes = getPayloadTypesInMediaSection(section); var vp8PayloadTypes = new Set(codecMap.get('vp8') || []); var hasVP8PayloadType = payloadTypes.some(function (payloadType) { return vp8PayloadTypes.has(payloadType); }); return hasVP8PayloadType ? setSimulcastInMediaSection(section, trackIdsToAttributes) : section; })).concat('').join('\r\n'); } /** * Get the matching Payload Types in an m= section for a particular peer codec. * @param {Codec} peerCodec * @param {PT} peerPt * @param {Map<Codec, PT>} codecsToPts * @param {string} section * @param {string} peerSection * @returns {Array<PT>} */ function getMatchingPayloadTypes(peerCodec, peerPt, codecsToPts, section, peerSection) { // If there is at most one local Payload Type that matches the remote codec, retain it. var matchingPts = codecsToPts.get(peerCodec) || []; if (matchingPts.length <= 1) { return matchingPts; } // If there are no fmtp attributes for the codec in the peer m= section, then we // cannot get a match in the m= section. In that case, retain all matching Payload // Types. var peerFmtpAttrs = getFmtpAttributesForPt(peerPt, peerSection); if (!peerFmtpAttrs) { return matchingPts; } // Among the matched local Payload Types, find the one that matches the remote // fmtp attributes. var matchingPt = matchingPts.find(function (pt) { var fmtpAttrs = getFmtpAttributesForPt(pt, section); return fmtpAttrs && Object.keys(peerFmtpAttrs).every(function (attr) { return peerFmtpAttrs[attr] === fmtpAttrs[attr]; }); }); // If none of the matched Payload Types also have matching fmtp attributes, // then retain all of them, otherwise retain only the Payload Type that // matches the peer fmtp attributes. return typeof matchingPt === 'number' ? [matchingPt] : matchingPts; } /** * Filter codecs in an m= section based on its peer m= section from the other peer. * @param {string} section * @param {Map<string, string>} peerMidsToMediaSections * @param {Array<string>} codecsToRemove * @returns {string} */ function filterCodecsInMediaSection(section, peerMidsToMediaSections, codecsToRemove) { // Do nothing if the m= section represents neither audio nor video. if (!/^m=(audio|video)/.test(section)) { return section; } // Do nothing if the m= section does not have an equivalent remote m= section. var mid = getMidForMediaSection(section); var peerSection = mid && peerMidsToMediaSections.get(mid); if (!peerSection) { return section; } // Construct a Map of the peer Payload Types to their codec names. var peerPtToCodecs = createPtToCodecName(peerSection); // Construct a Map of the codec names to their Payload Types. var codecsToPts = createCodecMapForMediaSection(section); // Maintain a list of non-rtx Payload Types to retain. var pts = flatMap(Array.from(peerPtToCodecs), function (_a) { var _b = __read(_a, 2), peerPt = _b[0], peerCodec = _b[1]; return peerCodec !== 'rtx' && !codecsToRemove.includes(peerCodec) ? getMatchingPayloadTypes(peerCodec, peerPt, codecsToPts, section, peerSection) : []; }); // For each Payload Type that will be retained, retain their corresponding rtx // Payload Type if present. var rtxPts = codecsToPts.get('rtx') || []; // In "a=fmtp:<rtxPt> apt=<apt>", extract the codec PT <apt> associated with rtxPt. pts = pts.concat(rtxPts.filter(function (rtxPt) { var fmtpAttrs = getFmtpAttributesForPt(rtxPt, section); return fmtpAttrs && pts.includes(fmtpAttrs.apt); })); // Filter out the below mentioned attribute lines in the m= section that do not // belong to one of the Payload Types that are to be retained. // 1. "a=rtpmap:<pt> <codec>" // 2. "a=rtcp-fb:<pt> <attr>[ <attr>]*" // 3. "a=fmtp:<pt> <name>=<value>[;<name>=<value>]*" var lines = section.split('\r\n').filter(function (line) { var ptMatches = line.match(/^a=(rtpmap|fmtp|rtcp-fb):(.+) .+$/); var pt = ptMatches && ptMatches[2]; return !ptMatches || (pt && pts.includes(parseInt(pt, 10))); }); // Filter the list of Payload Types in the first line of the m= section. var orderedPts = getPayloadTypesInMediaSection(section).filter(function (pt) { return pts.includes(pt); }); return setPayloadTypesInMediaSection(orderedPts, lines.join('\r\n')); } /** * Filter local codecs based on the remote SDP. * @param {string} localSdp * @param {string} remoteSdp * @returns {string} - Updated local SDP */ function filterLocalCodecs(localSdp, remoteSdp) { var localMediaSections = getMediaSections(localSdp); var localSession = localSdp.split('\r\nm=')[0]; var remoteMidsToMediaSections = createMidToMediaSectionMap(remoteSdp); return [localSession].concat(localMediaSections.map(function (localSection) { return filterCodecsInMediaSection(localSection, remoteMidsToMediaSections, []); })).join('\r\n'); } /** * Return a new SDP string after reverting simulcast for non vp8 sections in remote sdp. * @param localSdp - simulcast enabled local sdp * @param localSdpWithoutSimulcast - local sdp before simulcast was set * @param remoteSdp - remote sdp * @param revertForAll - when true simulcast will be reverted for all codecs. when false it will be reverted * only for non-vp8 codecs. * @return {string} Updated SDP string */ function revertSimulcast(localSdp, localSdpWithoutSimulcast, remoteSdp, revertForAll) { if (revertForAll === void 0) { revertForAll = false; } var remoteMidToMediaSections = createMidToMediaSectionMap(remoteSdp); var localMidToMediaSectionsWithoutSimulcast = createMidToMediaSectionMap(localSdpWithoutSimulcast); var mediaSections = getMediaSections(localSdp); var session = localSdp.split('\r\nm=')[0]; return [session].concat(mediaSections.map(function (section) { section = section.replace(/\r\n$/, ''); if (!/^m=video/.test(section)) { return section; } var midMatches = section.match(/^a=mid:(.+)$/m); var mid = midMatches && midMatches[1]; if (!mid) { return section; } var remoteSection = remoteMidToMediaSections.get(mid); var remotePtToCodecs = createPtToCodecName(remoteSection); var remotePayloadTypes = getPayloadTypesInMediaSection(remoteSection); var isVP8ThePreferredCodec = remotePayloadTypes.length && remotePtToCodecs.get(remotePayloadTypes[0]) === 'vp8'; var shouldRevertSimulcast = revertForAll || !isVP8ThePreferredCodec; return shouldRevertSimulcast ? localMidToMediaSectionsWithoutSimulcast.get(mid).replace(/\r\n$/, '') : section; })).concat('').join('\r\n'); } /** * Add or rewrite MSIDs for new m= sections in the given SDP with their corresponding * local MediaStreamTrack IDs. These can be different when previously removed MediaStreamTracks * are added back (or Track IDs may not be present in the SDPs at all once browsers implement * the latest WebRTC spec). * @param {string} sdp * @param {Map<string, Track.ID>} activeMidsToTrackIds * @param {Map<Track.Kind, Array<Track.ID>>} trackIdsByKind * @returns {string} */ function addOrRewriteNewTrackIds(sdp, activeMidsToTrackIds, trackIdsByKind) { // NOTE(mmalavalli): The m= sections for the new MediaStreamTracks are usually // present after the m= sections for the existing MediaStreamTracks, in order // of addition. var newMidsToTrackIds = Array.from(trackIdsByKind).reduce(function (midsToTrackIds, _a) { var _b = __read(_a, 2), kind = _b[0], trackIds = _b[1]; var mediaSections = getMediaSections(sdp, kind, 'send(only|recv)'); var newMids = mediaSections.map(getMidForMediaSection).filter(function (mid) { return !activeMidsToTrackIds.has(mid); }); newMids.forEach(function (mid, i) { return midsToTrackIds.set(mid, trackIds[i]); }); return midsToTrackIds; }, new Map()); return addOrRewriteTrackIds(sdp, newMidsToTrackIds); } /** * Add or rewrite MSIDs in the given SDP with their corresponding local MediaStreamTrack IDs. * These IDs need not be the same (or Track IDs may not be present in the SDPs at all once * browsers implement the latest WebRTC spec). * @param {string} sdp * @param {Map<string, Track.ID>} midsToTrackIds * @returns {string} */ function addOrRewriteTrackIds(sdp, midsToTrackIds) { var mediaSections = getMediaSections(sdp); var session = sdp.split('\r\nm=')[0]; return [session].concat(mediaSections.map(function (mediaSection) { // Do nothing if the m= section represents neither audio nor video. if (!/^m=(audio|video)/.test(mediaSection)) { return mediaSection; } // This shouldn't happen, but in case there is no MID for the m= section, do nothing. var mid = getMidForMediaSection(mediaSection); if (!mid) { return mediaSection; } // In case there is no Track ID for the given MID in the map, do nothing. var trackId = midsToTrackIds.get(mid); if (!trackId) { return mediaSection; } // This shouldn't happen, but in case there is no a=msid: line, do nothing. var attributes = (mediaSection.match(/^a=msid:(.+)$/m) || [])[1]; if (!attributes) { return mediaSection; } // If the a=msid: line contains the "appdata" field, then replace it with the Track ID, // otherwise append the Track ID. var _a = __read(attributes.split(' '), 2), msid = _a[0], trackIdToRewrite = _a[1]; var msidRegex = new RegExp("msid:" + msid + (trackIdToRewrite ? " " + trackIdToRewrite : '') + "$", 'gm'); return mediaSection.replace(msidRegex, "msid:" + msid + " " + trackId); })).join('\r\n'); } /** * Removes specified ssrc attributes from given sdp. * @param {string} sdp * @param {Array<string>} ssrcAttributesToRemove * @returns {string} */ function removeSSRCAttributes(sdp, ssrcAttributesToRemove) { return sdp.split('\r\n').filter(function (line) { return !ssrcAttributesToRemove.find(function (srcAttribute) { return new RegExp('a=ssrc:.*' + srcAttribute + ':', 'g').test(line); }); }).join('\r\n'); } /** * Disable RTX in a given sdp. * @param {string} sdp * @returns {string} sdp without RTX */ function disableRtx(sdp) { var mediaSections = getMediaSections(sdp); var session = sdp.split('\r\nm=')[0]; return [session].concat(mediaSections.map(function (mediaSection) { // Do nothing if the m= section does not represent a video track. if (!/^m=video/.test(mediaSection)) { return mediaSection; } // Create a map of codecs to payload types. var codecsToPts = createCodecMapForMediaSection(mediaSection); // Get the RTX payload types. var rtxPts = codecsToPts.get('rtx'); // Do nothing if there are no RTX payload types. if (!rtxPts) { return mediaSection; } // Remove the RTX payload types. var pts = new Set(getPayloadTypesInMediaSection(mediaSection)); rtxPts.forEach(function (rtxPt) { return pts.delete(rtxPt); }); // Get the RTX SSRC. var rtxSSRCMatches = mediaSection.match(/a=ssrc-group:FID [0-9]+ ([0-9]+)/); var rtxSSRC = rtxSSRCMatches && rtxSSRCMatches[1]; // Remove the following lines associated with the RTX payload types: // 1. "a=fmtp:<rtxPt> apt=<pt>" // 2. "a=rtpmap:<rtxPt> rtx/..." // 3. "a=ssrc:<rtxSSRC> cname:..." // 4. "a=ssrc-group:FID <SSRC> <rtxSSRC>" var filterRegexes = [ /^a=fmtp:.+ apt=.+$/, /^a=rtpmap:.+ rtx\/.+$/, /^a=ssrc-group:.+$/ ].concat(rtxSSRC ? [new RegExp("^a=ssrc:" + rtxSSRC + " .+$")] : []); mediaSection = mediaSection.split('\r\n') .filter(function (line) { return filterRegexes.every(function (regex) { return !regex.test(line); }); }) .join('\r\n'); // Reconstruct the m= section without the RTX payload types. return setPayloadTypesInMediaSection(Array.from(pts), mediaSection); })).join('\r\n'); } /** * Generate an a=fmtp: line from the given payload type and attributes. * @param {PT} pt * @param {*} fmtpAttrs * @returns {string} */ function generateFmtpLineFromPtAndAttributes(pt, fmtpAttrs) { var serializedFmtpAttrs = Object.entries(fmtpAttrs).map(function (_a) { var _b = __read(_a, 2), name = _b[0], value = _b[1]; return name + "=" + value; }).join(';'); return "a=fmtp:" + pt + " " + serializedFmtpAttrs; } /** * Enable DTX for opus in the m= sections for the given MIDs.` * @param {string} sdp * @param {Array<string>} [mids] - If not specified, enables opus DTX for all * audio m= lines. * @returns {string} */ function enableDtxForOpus(sdp, mids) { var mediaSections = getMediaSections(sdp); var session = sdp.split('\r\nm=')[0]; mids = mids || mediaSections .filter(function (section) { return /^m=audio/.test(section); }) .map(getMidForMediaSection); return [session].concat(mediaSections.map(function (section) { // Do nothing if the m= section is not audio. if (!/^m=audio/.test(section)) { return section; } // Build a map codecs to payload types. var codecsToPts = createCodecMapForMediaSection(section); // Do nothing if a payload type for opus does not exist. var opusPt = codecsToPts.get('opus'); if (!opusPt) { return section; } // If no fmtp attributes are found for opus, do nothing. var opusFmtpAttrs = getFmtpAttributesForPt(opusPt, section); if (!opusFmtpAttrs) { return section; } // Add usedtx=1 to the a=fmtp: line for opus. var origOpusFmtpLine = generateFmtpLineFromPtAndAttributes(opusPt, opusFmtpAttrs); var origOpusFmtpRegex = new RegExp(origOpusFmtpLine); // If the m= section's MID is in the list of MIDs, then enable dtx. Otherwise disable it. var mid = getMidForMediaSection(section); if (mids.includes(mid)) { opusFmtpAttrs.usedtx = 1; } else { delete opusFmtpAttrs.usedtx; } var opusFmtpLineWithDtx = generateFmtpLineFromPtAndAttributes(opusPt, opusFmtpAttrs); return section.replace(origOpusFmtpRegex, opusFmtpLineWithDtx); })).join('\r\n'); } exports.addOrRewriteNewTrackIds = addOrRewriteNewTrackIds; exports.addOrRewriteTrackIds = addOrRewriteTrackIds; exports.createCodecMapForMediaSection = createCodecMapForMediaSection; exports.createPtToCodecName = createPtToCodecName; exports.disableRtx = disableRtx; exports.enableDtxForOpus = enableDtxForOpus; exports.filterLocalCodecs = filterLocalCodecs; exports.getMediaSections = getMediaSections; exports.removeSSRCAttributes = removeSSRCAttributes; exports.revertSimulcast = revertSimulcast; exports.setCodecPreferences = setCodecPreferences; exports.setSimulcast = setSimulcast; //# sourceMappingURL=index.js.map