twilio-video
Version:
Twilio Video JavaScript Library
466 lines • 20.7 kB
JavaScript
;
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) {
// NOTE(lrivas): Ignore repeated PTs to prevent RFC non‑compliant SDP generation.
// See, https://github.com/twilio/twilio-video.js/issues/2122.
if (ptToCodecName.has(pt)) {
return ptToCodecName;
}
var rtpmapPattern = new RegExp("a=rtpmap:".concat(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:".concat(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=".concat(mediaSection); }).filter(function (mediaSection) {
var kindPattern = new RegExp("m=".concat(kind || '.*'), 'gm');
var directionPattern = new RegExp("a=".concat(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); });
}
/**
* 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 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');
}
/**
* 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:".concat(msid).concat(trackIdToRewrite ? " ".concat(trackIdToRewrite) : '', "$"), 'gm');
return mediaSection.replace(msidRegex, "msid:".concat(msid, " ").concat(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:".concat(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 "".concat(name, "=").concat(value);
}).join(';');
return "a=fmtp:".concat(pt, " ").concat(serializedFmtpAttrs);
}
/**
* Remove codec-related attributes (fmtp, rtpmap, rtcp-fb) for payload types
* that are not present in the m= line. This works around a Firefox bug where
* setCodecPreferences leaves unreferenced attributes after filtering codecs.
* @param {string} sdp
* @returns {string} sdp without unreferenced codec attributes
*/
function removeUnreferencedCodecAttributes(sdp) {
var mediaSections = getMediaSections(sdp);
var session = sdp.split('\r\nm=')[0];
return [session].concat(mediaSections.map(function (mediaSection) {
if (!/^m=(audio|video)/.test(mediaSection)) {
return mediaSection;
}
var validPts = new Set(getPayloadTypesInMediaSection(mediaSection));
return mediaSection.split('\r\n').filter(function (line) {
var ptMatch = line.match(/^a=(fmtp|rtpmap|rtcp-fb):(\d+)/);
if (!ptMatch) {
return true;
}
var pt = parseInt(ptMatch[2], 10);
return validPts.has(pt);
}).join('\r\n');
})).join('\r\n');
}
/**
* 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');
}
/**
* 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);
}
/**
* Return a new SDP string with the re-ordered codec preferences.
* Used for remote descriptions to enable asymmetric codec support in P2P.
* @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');
}
exports.addOrRewriteNewTrackIds = addOrRewriteNewTrackIds;
exports.addOrRewriteTrackIds = addOrRewriteTrackIds;
exports.createCodecMapForMediaSection = createCodecMapForMediaSection;
exports.createPtToCodecName = createPtToCodecName;
exports.disableRtx = disableRtx;
exports.enableDtxForOpus = enableDtxForOpus;
exports.getMediaSections = getMediaSections;
exports.removeUnreferencedCodecAttributes = removeUnreferencedCodecAttributes;
exports.removeSSRCAttributes = removeSSRCAttributes;
exports.revertSimulcast = revertSimulcast;
exports.setCodecPreferences = setCodecPreferences;
exports.setSimulcast = setSimulcast;
//# sourceMappingURL=index.js.map