视频会议

This commit is contained in:
wangqiong
2024-01-18 10:46:33 +08:00
parent d7f5ce05ea
commit 37a88df277
28922 changed files with 2797396 additions and 19461 deletions
Generated Vendored
+61
View File
@@ -0,0 +1,61 @@
{
"rules": {
"array-bracket-spacing": 2,
"block-spacing": [2, "never"],
"brace-style": [2, "1tbs", {"allowSingleLine": false}],
"camelcase": [2, {"properties": "always"}],
"curly": 2,
"default-case": 2,
"dot-notation": 2,
"eqeqeq": 2,
"indent": [
2,
2,
{"SwitchCase": 1}
],
"key-spacing": [2, {"beforeColon": false, "afterColon": true}],
"max-len": [2, 80, 2, {"ignoreUrls": true}],
"new-cap": [2, {"newIsCapExceptions": [
"webkitRTCPeerConnection",
"mozRTCPeerConnection"
]}],
"no-console": 0,
"no-else-return": 2,
"no-eval": 2,
"no-multi-spaces": 2,
"no-multiple-empty-lines": [2, {"max": 2}],
"no-shadow": 2,
"no-trailing-spaces": 2,
"no-unused-expressions": 2,
"no-unused-vars": [2, {"args": "none"}],
"object-curly-spacing": [2, "never"],
"padded-blocks": [2, "never"],
"quotes": [
2,
"single"
],
"semi": [
2,
"always"
],
"keyword-spacing": 2,
"space-before-blocks": 2,
"space-before-function-paren": [2, "never"],
"space-unary-ops": 2,
"space-infix-ops": 2,
"spaced-comment": 2,
"valid-typeof": 2
},
"env": {
"es6": true,
"browser": true,
"node": true
},
"extends": ["eslint:recommended"],
"globals": {
"module": true,
"require": true,
"process": true,
"Promise": true,
}
}
+16
View File
@@ -0,0 +1,16 @@
name: npm-test
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- run: npm test
- run: npm run coverage
- uses: codecov/codecov-action@v1
with:
file: ./coverage.lcov
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"uuid":"a9b935d8-5f14-4c64-b895-6b0ece2e9d7c","parent":null,"pid":5750,"argv":["/usr/bin/node","/media/fippo/houseparty/webrtc/sdp/node_modules/.bin/mocha","test/sdp.js"],"execArgv":[],"cwd":"/media/fippo/houseparty/webrtc/sdp","time":1673267457493,"ppid":5743,"root":"9110d26c-7a83-40c7-a41d-9670d698bc02","coverageFilename":"/media/fippo/houseparty/webrtc/sdp/.nyc_output/a9b935d8-5f14-4c64-b895-6b0ece2e9d7c.json","files":["/media/fippo/houseparty/webrtc/sdp/sdp.js"]}
+1
View File
@@ -0,0 +1 @@
{"processes":{"a9b935d8-5f14-4c64-b895-6b0ece2e9d7c":{"parent":null,"children":[]}},"files":{"/media/fippo/houseparty/webrtc/sdp/sdp.js":["a9b935d8-5f14-4c64-b895-6b0ece2e9d7c"]},"externalIds":{}}
Generated Vendored
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2017 Philipp Hancke
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Generated Vendored
+4
View File
@@ -0,0 +1,4 @@
# SDP parsing and utlities
originally for generating SDP from ORTC and vice versa -- see https://github.com/fippo/adapter/tree/shim-ortc
But it turned out to be useful for manipulating SDP outside the context of Microsofts Edge browser.
+1083
View File
File diff suppressed because it is too large Load Diff
+773
View File
@@ -0,0 +1,773 @@
/* eslint-env node */
'use strict';
// SDP helpers.
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var SDPUtils = {};
// Generate an alphanumeric identifier for cname or mids.
// TODO: use UUIDs instead? https://gist.github.com/jed/982883
SDPUtils.generateIdentifier = function () {
return Math.random().toString(36).substring(2, 12);
};
// The RTCP CNAME used by all peerconnections from the same JS.
SDPUtils.localCName = SDPUtils.generateIdentifier();
// Splits SDP into lines, dealing with both CRLF and LF.
SDPUtils.splitLines = function (blob) {
return blob.trim().split('\n').map(function (line) {
return line.trim();
});
};
// Splits SDP into sessionpart and mediasections. Ensures CRLF.
SDPUtils.splitSections = function (blob) {
var parts = blob.split('\nm=');
return parts.map(function (part, index) {
return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
});
};
// Returns the session description.
SDPUtils.getDescription = function (blob) {
var sections = SDPUtils.splitSections(blob);
return sections && sections[0];
};
// Returns the individual media sections.
SDPUtils.getMediaSections = function (blob) {
var sections = SDPUtils.splitSections(blob);
sections.shift();
return sections;
};
// Returns lines that start with a certain prefix.
SDPUtils.matchPrefix = function (blob, prefix) {
return SDPUtils.splitLines(blob).filter(function (line) {
return line.indexOf(prefix) === 0;
});
};
// Parses an ICE candidate line. Sample input:
// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
// rport 55996"
// Input can be prefixed with a=.
SDPUtils.parseCandidate = function (line) {
var parts = void 0;
// Parse both variants.
if (line.indexOf('a=candidate:') === 0) {
parts = line.substring(12).split(' ');
} else {
parts = line.substring(10).split(' ');
}
var candidate = {
foundation: parts[0],
component: { 1: 'rtp', 2: 'rtcp' }[parts[1]] || parts[1],
protocol: parts[2].toLowerCase(),
priority: parseInt(parts[3], 10),
ip: parts[4],
address: parts[4], // address is an alias for ip.
port: parseInt(parts[5], 10),
// skip parts[6] == 'typ'
type: parts[7]
};
for (var i = 8; i < parts.length; i += 2) {
switch (parts[i]) {
case 'raddr':
candidate.relatedAddress = parts[i + 1];
break;
case 'rport':
candidate.relatedPort = parseInt(parts[i + 1], 10);
break;
case 'tcptype':
candidate.tcpType = parts[i + 1];
break;
case 'ufrag':
candidate.ufrag = parts[i + 1]; // for backward compatibility.
candidate.usernameFragment = parts[i + 1];
break;
default:
// extension handling, in particular ufrag. Don't overwrite.
if (candidate[parts[i]] === undefined) {
candidate[parts[i]] = parts[i + 1];
}
break;
}
}
return candidate;
};
// Translates a candidate object into SDP candidate attribute.
// This does not include the a= prefix!
SDPUtils.writeCandidate = function (candidate) {
var sdp = [];
sdp.push(candidate.foundation);
var component = candidate.component;
if (component === 'rtp') {
sdp.push(1);
} else if (component === 'rtcp') {
sdp.push(2);
} else {
sdp.push(component);
}
sdp.push(candidate.protocol.toUpperCase());
sdp.push(candidate.priority);
sdp.push(candidate.address || candidate.ip);
sdp.push(candidate.port);
var type = candidate.type;
sdp.push('typ');
sdp.push(type);
if (type !== 'host' && candidate.relatedAddress && candidate.relatedPort) {
sdp.push('raddr');
sdp.push(candidate.relatedAddress);
sdp.push('rport');
sdp.push(candidate.relatedPort);
}
if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
sdp.push('tcptype');
sdp.push(candidate.tcpType);
}
if (candidate.usernameFragment || candidate.ufrag) {
sdp.push('ufrag');
sdp.push(candidate.usernameFragment || candidate.ufrag);
}
return 'candidate:' + sdp.join(' ');
};
// Parses an ice-options line, returns an array of option tags.
// Sample input:
// a=ice-options:foo bar
SDPUtils.parseIceOptions = function (line) {
return line.substring(14).split(' ');
};
// Parses a rtpmap line, returns RTCRtpCoddecParameters. Sample input:
// a=rtpmap:111 opus/48000/2
SDPUtils.parseRtpMap = function (line) {
var parts = line.substring(9).split(' ');
var parsed = {
payloadType: parseInt(parts.shift(), 10) // was: id
};
parts = parts[0].split('/');
parsed.name = parts[0];
parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
// legacy alias, got renamed back to channels in ORTC.
parsed.numChannels = parsed.channels;
return parsed;
};
// Generates a rtpmap line from RTCRtpCodecCapability or
// RTCRtpCodecParameters.
SDPUtils.writeRtpMap = function (codec) {
var pt = codec.payloadType;
if (codec.preferredPayloadType !== undefined) {
pt = codec.preferredPayloadType;
}
var channels = codec.channels || codec.numChannels || 1;
return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + (channels !== 1 ? '/' + channels : '') + '\r\n';
};
// Parses a extmap line (headerextension from RFC 5285). Sample input:
// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
SDPUtils.parseExtmap = function (line) {
var parts = line.substring(9).split(' ');
return {
id: parseInt(parts[0], 10),
direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
uri: parts[1],
attributes: parts.slice(2).join(' ')
};
};
// Generates an extmap line from RTCRtpHeaderExtensionParameters or
// RTCRtpHeaderExtension.
SDPUtils.writeExtmap = function (headerExtension) {
return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + (headerExtension.direction && headerExtension.direction !== 'sendrecv' ? '/' + headerExtension.direction : '') + ' ' + headerExtension.uri + (headerExtension.attributes ? ' ' + headerExtension.attributes : '') + '\r\n';
};
// Parses a fmtp line, returns dictionary. Sample input:
// a=fmtp:96 vbr=on;cng=on
// Also deals with vbr=on; cng=on
SDPUtils.parseFmtp = function (line) {
var parsed = {};
var kv = void 0;
var parts = line.substring(line.indexOf(' ') + 1).split(';');
for (var j = 0; j < parts.length; j++) {
kv = parts[j].trim().split('=');
parsed[kv[0].trim()] = kv[1];
}
return parsed;
};
// Generates a fmtp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
SDPUtils.writeFmtp = function (codec) {
var line = '';
var pt = codec.payloadType;
if (codec.preferredPayloadType !== undefined) {
pt = codec.preferredPayloadType;
}
if (codec.parameters && Object.keys(codec.parameters).length) {
var params = [];
Object.keys(codec.parameters).forEach(function (param) {
if (codec.parameters[param] !== undefined) {
params.push(param + '=' + codec.parameters[param]);
} else {
params.push(param);
}
});
line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
}
return line;
};
// Parses a rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
// a=rtcp-fb:98 nack rpsi
SDPUtils.parseRtcpFb = function (line) {
var parts = line.substring(line.indexOf(' ') + 1).split(' ');
return {
type: parts.shift(),
parameter: parts.join(' ')
};
};
// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
SDPUtils.writeRtcpFb = function (codec) {
var lines = '';
var pt = codec.payloadType;
if (codec.preferredPayloadType !== undefined) {
pt = codec.preferredPayloadType;
}
if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
// FIXME: special handling for trr-int?
codec.rtcpFeedback.forEach(function (fb) {
lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + '\r\n';
});
}
return lines;
};
// Parses a RFC 5576 ssrc media attribute. Sample input:
// a=ssrc:3735928559 cname:something
SDPUtils.parseSsrcMedia = function (line) {
var sp = line.indexOf(' ');
var parts = {
ssrc: parseInt(line.substring(7, sp), 10)
};
var colon = line.indexOf(':', sp);
if (colon > -1) {
parts.attribute = line.substring(sp + 1, colon);
parts.value = line.substring(colon + 1);
} else {
parts.attribute = line.substring(sp + 1);
}
return parts;
};
// Parse a ssrc-group line (see RFC 5576). Sample input:
// a=ssrc-group:semantics 12 34
SDPUtils.parseSsrcGroup = function (line) {
var parts = line.substring(13).split(' ');
return {
semantics: parts.shift(),
ssrcs: parts.map(function (ssrc) {
return parseInt(ssrc, 10);
})
};
};
// Extracts the MID (RFC 5888) from a media section.
// Returns the MID or undefined if no mid line was found.
SDPUtils.getMid = function (mediaSection) {
var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
if (mid) {
return mid.substring(6);
}
};
// Parses a fingerprint line for DTLS-SRTP.
SDPUtils.parseFingerprint = function (line) {
var parts = line.substring(14).split(' ');
return {
algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
value: parts[1].toUpperCase() // the definition is upper-case in RFC 4572.
};
};
// Extracts DTLS parameters from SDP media section or sessionpart.
// FIXME: for consistency with other functions this should only
// get the fingerprint line as input. See also getIceParameters.
SDPUtils.getDtlsParameters = function (mediaSection, sessionpart) {
var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=fingerprint:');
// Note: a=setup line is ignored since we use the 'auto' role in Edge.
return {
role: 'auto',
fingerprints: lines.map(SDPUtils.parseFingerprint)
};
};
// Serializes DTLS parameters to SDP.
SDPUtils.writeDtlsParameters = function (params, setupType) {
var sdp = 'a=setup:' + setupType + '\r\n';
params.fingerprints.forEach(function (fp) {
sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
});
return sdp;
};
// Parses a=crypto lines into
// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members
SDPUtils.parseCryptoLine = function (line) {
var parts = line.substring(9).split(' ');
return {
tag: parseInt(parts[0], 10),
cryptoSuite: parts[1],
keyParams: parts[2],
sessionParams: parts.slice(3)
};
};
SDPUtils.writeCryptoLine = function (parameters) {
return 'a=crypto:' + parameters.tag + ' ' + parameters.cryptoSuite + ' ' + (_typeof(parameters.keyParams) === 'object' ? SDPUtils.writeCryptoKeyParams(parameters.keyParams) : parameters.keyParams) + (parameters.sessionParams ? ' ' + parameters.sessionParams.join(' ') : '') + '\r\n';
};
// Parses the crypto key parameters into
// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam*
SDPUtils.parseCryptoKeyParams = function (keyParams) {
if (keyParams.indexOf('inline:') !== 0) {
return null;
}
var parts = keyParams.substring(7).split('|');
return {
keyMethod: 'inline',
keySalt: parts[0],
lifeTime: parts[1],
mkiValue: parts[2] ? parts[2].split(':')[0] : undefined,
mkiLength: parts[2] ? parts[2].split(':')[1] : undefined
};
};
SDPUtils.writeCryptoKeyParams = function (keyParams) {
return keyParams.keyMethod + ':' + keyParams.keySalt + (keyParams.lifeTime ? '|' + keyParams.lifeTime : '') + (keyParams.mkiValue && keyParams.mkiLength ? '|' + keyParams.mkiValue + ':' + keyParams.mkiLength : '');
};
// Extracts all SDES parameters.
SDPUtils.getCryptoParameters = function (mediaSection, sessionpart) {
var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=crypto:');
return lines.map(SDPUtils.parseCryptoLine);
};
// Parses ICE information from SDP media section or sessionpart.
// FIXME: for consistency with other functions this should only
// get the ice-ufrag and ice-pwd lines as input.
SDPUtils.getIceParameters = function (mediaSection, sessionpart) {
var ufrag = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=ice-ufrag:')[0];
var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=ice-pwd:')[0];
if (!(ufrag && pwd)) {
return null;
}
return {
usernameFragment: ufrag.substring(12),
password: pwd.substring(10)
};
};
// Serializes ICE parameters to SDP.
SDPUtils.writeIceParameters = function (params) {
var sdp = 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 'a=ice-pwd:' + params.password + '\r\n';
if (params.iceLite) {
sdp += 'a=ice-lite\r\n';
}
return sdp;
};
// Parses the SDP media section and returns RTCRtpParameters.
SDPUtils.parseRtpParameters = function (mediaSection) {
var description = {
codecs: [],
headerExtensions: [],
fecMechanisms: [],
rtcp: []
};
var lines = SDPUtils.splitLines(mediaSection);
var mline = lines[0].split(' ');
description.profile = mline[2];
for (var i = 3; i < mline.length; i++) {
// find all codecs from mline[3..]
var pt = mline[i];
var rtpmapline = SDPUtils.matchPrefix(mediaSection, 'a=rtpmap:' + pt + ' ')[0];
if (rtpmapline) {
var codec = SDPUtils.parseRtpMap(rtpmapline);
var fmtps = SDPUtils.matchPrefix(mediaSection, 'a=fmtp:' + pt + ' ');
// Only the first a=fmtp:<pt> is considered.
codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
codec.rtcpFeedback = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:' + pt + ' ').map(SDPUtils.parseRtcpFb);
description.codecs.push(codec);
// parse FEC mechanisms from rtpmap lines.
switch (codec.name.toUpperCase()) {
case 'RED':
case 'ULPFEC':
description.fecMechanisms.push(codec.name.toUpperCase());
break;
default:
// only RED and ULPFEC are recognized as FEC mechanisms.
break;
}
}
}
SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function (line) {
description.headerExtensions.push(SDPUtils.parseExtmap(line));
});
var wildcardRtcpFb = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:* ').map(SDPUtils.parseRtcpFb);
description.codecs.forEach(function (codec) {
wildcardRtcpFb.forEach(function (fb) {
var duplicate = codec.rtcpFeedback.find(function (existingFeedback) {
return existingFeedback.type === fb.type && existingFeedback.parameter === fb.parameter;
});
if (!duplicate) {
codec.rtcpFeedback.push(fb);
}
});
});
// FIXME: parse rtcp.
return description;
};
// Generates parts of the SDP media section describing the capabilities /
// parameters.
SDPUtils.writeRtpDescription = function (kind, caps) {
var sdp = '';
// Build the mline.
sdp += 'm=' + kind + ' ';
sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
sdp += ' ' + (caps.profile || 'UDP/TLS/RTP/SAVPF') + ' ';
sdp += caps.codecs.map(function (codec) {
if (codec.preferredPayloadType !== undefined) {
return codec.preferredPayloadType;
}
return codec.payloadType;
}).join(' ') + '\r\n';
sdp += 'c=IN IP4 0.0.0.0\r\n';
sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
// Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
caps.codecs.forEach(function (codec) {
sdp += SDPUtils.writeRtpMap(codec);
sdp += SDPUtils.writeFmtp(codec);
sdp += SDPUtils.writeRtcpFb(codec);
});
var maxptime = 0;
caps.codecs.forEach(function (codec) {
if (codec.maxptime > maxptime) {
maxptime = codec.maxptime;
}
});
if (maxptime > 0) {
sdp += 'a=maxptime:' + maxptime + '\r\n';
}
if (caps.headerExtensions) {
caps.headerExtensions.forEach(function (extension) {
sdp += SDPUtils.writeExtmap(extension);
});
}
// FIXME: write fecMechanisms.
return sdp;
};
// Parses the SDP media section and returns an array of
// RTCRtpEncodingParameters.
SDPUtils.parseRtpEncodingParameters = function (mediaSection) {
var encodingParameters = [];
var description = SDPUtils.parseRtpParameters(mediaSection);
var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
// filter a=ssrc:... cname:, ignore PlanB-msid
var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) {
return SDPUtils.parseSsrcMedia(line);
}).filter(function (parts) {
return parts.attribute === 'cname';
});
var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
var secondarySsrc = void 0;
var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID').map(function (line) {
var parts = line.substring(17).split(' ');
return parts.map(function (part) {
return parseInt(part, 10);
});
});
if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
secondarySsrc = flows[0][1];
}
description.codecs.forEach(function (codec) {
if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
var encParam = {
ssrc: primarySsrc,
codecPayloadType: parseInt(codec.parameters.apt, 10)
};
if (primarySsrc && secondarySsrc) {
encParam.rtx = { ssrc: secondarySsrc };
}
encodingParameters.push(encParam);
if (hasRed) {
encParam = JSON.parse(JSON.stringify(encParam));
encParam.fec = {
ssrc: primarySsrc,
mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
};
encodingParameters.push(encParam);
}
}
});
if (encodingParameters.length === 0 && primarySsrc) {
encodingParameters.push({
ssrc: primarySsrc
});
}
// we support both b=AS and b=TIAS but interpret AS as TIAS.
var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
if (bandwidth.length) {
if (bandwidth[0].indexOf('b=TIAS:') === 0) {
bandwidth = parseInt(bandwidth[0].substring(7), 10);
} else if (bandwidth[0].indexOf('b=AS:') === 0) {
// use formula from JSEP to convert b=AS to TIAS value.
bandwidth = parseInt(bandwidth[0].substring(5), 10) * 1000 * 0.95 - 50 * 40 * 8;
} else {
bandwidth = undefined;
}
encodingParameters.forEach(function (params) {
params.maxBitrate = bandwidth;
});
}
return encodingParameters;
};
// parses http://draft.ortc.org/#rtcrtcpparameters*
SDPUtils.parseRtcpParameters = function (mediaSection) {
var rtcpParameters = {};
// Gets the first SSRC. Note that with RTX there might be multiple
// SSRCs.
var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) {
return SDPUtils.parseSsrcMedia(line);
}).filter(function (obj) {
return obj.attribute === 'cname';
})[0];
if (remoteSsrc) {
rtcpParameters.cname = remoteSsrc.value;
rtcpParameters.ssrc = remoteSsrc.ssrc;
}
// Edge uses the compound attribute instead of reducedSize
// compound is !reducedSize
var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
rtcpParameters.reducedSize = rsize.length > 0;
rtcpParameters.compound = rsize.length === 0;
// parses the rtcp-mux attrіbute.
// Note that Edge does not support unmuxed RTCP.
var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
rtcpParameters.mux = mux.length > 0;
return rtcpParameters;
};
SDPUtils.writeRtcpParameters = function (rtcpParameters) {
var sdp = '';
if (rtcpParameters.reducedSize) {
sdp += 'a=rtcp-rsize\r\n';
}
if (rtcpParameters.mux) {
sdp += 'a=rtcp-mux\r\n';
}
if (rtcpParameters.ssrc !== undefined && rtcpParameters.cname) {
sdp += 'a=ssrc:' + rtcpParameters.ssrc + ' cname:' + rtcpParameters.cname + '\r\n';
}
return sdp;
};
// parses either a=msid: or a=ssrc:... msid lines and returns
// the id of the MediaStream and MediaStreamTrack.
SDPUtils.parseMsid = function (mediaSection) {
var parts = void 0;
var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
if (spec.length === 1) {
parts = spec[0].substring(7).split(' ');
return { stream: parts[0], track: parts[1] };
}
var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) {
return SDPUtils.parseSsrcMedia(line);
}).filter(function (msidParts) {
return msidParts.attribute === 'msid';
});
if (planB.length > 0) {
parts = planB[0].value.split(' ');
return { stream: parts[0], track: parts[1] };
}
};
// SCTP
// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back
// to draft-ietf-mmusic-sctp-sdp-05
SDPUtils.parseSctpDescription = function (mediaSection) {
var mline = SDPUtils.parseMLine(mediaSection);
var maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:');
var maxMessageSize = void 0;
if (maxSizeLine.length > 0) {
maxMessageSize = parseInt(maxSizeLine[0].substring(19), 10);
}
if (isNaN(maxMessageSize)) {
maxMessageSize = 65536;
}
var sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:');
if (sctpPort.length > 0) {
return {
port: parseInt(sctpPort[0].substring(12), 10),
protocol: mline.fmt,
maxMessageSize: maxMessageSize
};
}
var sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:');
if (sctpMapLines.length > 0) {
var parts = sctpMapLines[0].substring(10).split(' ');
return {
port: parseInt(parts[0], 10),
protocol: parts[1],
maxMessageSize: maxMessageSize
};
}
};
// SCTP
// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers
// support by now receiving in this format, unless we originally parsed
// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line
// protocol of DTLS/SCTP -- without UDP/ or TCP/)
SDPUtils.writeSctpDescription = function (media, sctp) {
var output = [];
if (media.protocol !== 'DTLS/SCTP') {
output = ['m=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctp-port:' + sctp.port + '\r\n'];
} else {
output = ['m=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n'];
}
if (sctp.maxMessageSize !== undefined) {
output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n');
}
return output.join('');
};
// Generate a session ID for SDP.
// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
// recommends using a cryptographically random +ve 64-bit value
// but right now this should be acceptable and within the right range
SDPUtils.generateSessionId = function () {
return Math.random().toString().substr(2, 22);
};
// Write boiler plate for start of SDP
// sessId argument is optional - if not supplied it will
// be generated randomly
// sessVersion is optional and defaults to 2
// sessUser is optional and defaults to 'thisisadapterortc'
SDPUtils.writeSessionBoilerplate = function (sessId, sessVer, sessUser) {
var sessionId = void 0;
var version = sessVer !== undefined ? sessVer : 2;
if (sessId) {
sessionId = sessId;
} else {
sessionId = SDPUtils.generateSessionId();
}
var user = sessUser || 'thisisadapterortc';
// FIXME: sess-id should be an NTP timestamp.
return 'v=0\r\n' + 'o=' + user + ' ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' + 's=-\r\n' + 't=0 0\r\n';
};
// Gets the direction from the mediaSection or the sessionpart.
SDPUtils.getDirection = function (mediaSection, sessionpart) {
// Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
var lines = SDPUtils.splitLines(mediaSection);
for (var i = 0; i < lines.length; i++) {
switch (lines[i]) {
case 'a=sendrecv':
case 'a=sendonly':
case 'a=recvonly':
case 'a=inactive':
return lines[i].substring(2);
default:
// FIXME: What should happen here?
}
}
if (sessionpart) {
return SDPUtils.getDirection(sessionpart);
}
return 'sendrecv';
};
SDPUtils.getKind = function (mediaSection) {
var lines = SDPUtils.splitLines(mediaSection);
var mline = lines[0].split(' ');
return mline[0].substring(2);
};
SDPUtils.isRejected = function (mediaSection) {
return mediaSection.split(' ', 2)[1] === '0';
};
SDPUtils.parseMLine = function (mediaSection) {
var lines = SDPUtils.splitLines(mediaSection);
var parts = lines[0].substring(2).split(' ');
return {
kind: parts[0],
port: parseInt(parts[1], 10),
protocol: parts[2],
fmt: parts.slice(3).join(' ')
};
};
SDPUtils.parseOLine = function (mediaSection) {
var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0];
var parts = line.substring(2).split(' ');
return {
username: parts[0],
sessionId: parts[1],
sessionVersion: parseInt(parts[2], 10),
netType: parts[3],
addressType: parts[4],
address: parts[5]
};
};
// a very naive interpretation of a valid SDP.
SDPUtils.isValidSDP = function (blob) {
if (typeof blob !== 'string' || blob.length === 0) {
return false;
}
var lines = SDPUtils.splitLines(blob);
for (var i = 0; i < lines.length; i++) {
if (lines[i].length < 2 || lines[i].charAt(1) !== '=') {
return false;
}
// TODO: check the modifier a bit more.
}
return true;
};
// Expose public methods.
if ((typeof module === 'undefined' ? 'undefined' : _typeof(module)) === 'object') {
module.exports = SDPUtils;
}
+36
View File
@@ -0,0 +1,36 @@
{
"name": "sdp",
"version": "3.2.0",
"description": "SDP parsing and serialization utilities",
"main": "dist/sdp.js",
"module": "sdp.js",
"typings": "sdp.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/fippo/sdp.git"
},
"scripts": {
"build": "babel sdp.js --presets babel-preset-env --out-dir dist",
"coverage": "c8 report --reporter=text-lcov > coverage.lcov",
"prepare": "npm run build",
"test": "eslint sdp.js test/sdp.js && c8 --reporter html mocha test/sdp.js"
},
"keywords": [
"sdp",
"webrtc"
],
"author": "Philipp Hancke",
"license": "MIT",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"c8": "^7.12.0",
"chai": "^4.3.7",
"codecov": "^3.6.5",
"eslint": "^8.31.0",
"mocha": "^10.2.0",
"sinon": "^15.0.1",
"sinon-chai": "^3.7.0"
}
}
Generated Vendored
+220
View File
@@ -0,0 +1,220 @@
export type SDPBlob = string;
export type SDPLine = string;
export type SDPSection = string;
export type SDPDirection = 'sendonly' | 'recvonly' | 'sendrecv' | 'inactive';
export interface SDPIceCandidate {
foundation: string;
component: 'rtp' | 'rtcp' | number;
protocol: 'tcp' | 'udp';
priority: number;
ip: string;
address: string;
port: number;
type: 'host' | 'prflx' | 'srflx' | 'relay';
relatedAddress?: string;
relatedPort?: number;
tcpType?: string;
ufrag?: string;
usernameFragment?: string;
}
export interface SDPIceParameters {
iceLite?: boolean;
usernameFragment: string;
password: string;
}
export interface SDPCodecParameters {
payloadType: number;
preferredPayloadType?: number;
name: string;
clockRate: number;
channels: number;
numChannels?: number;
maxptime?: number;
}
export interface SDPCodecAdditionalParameters {
[key: string]: string;
}
export interface SDPHeaderExtension {
id: number;
direction?: SDPDirection;
uri: string;
atrributes: string | undefined;
}
export interface SDPFeedbackParameter {
type: string;
parameter: string;
}
export interface SDPFingerprint {
algorithm: string;
value: string;
}
export interface SDPDtlsParameters {
role: string;
fingerprints: SDPFingerprint[];
}
export interface SDPMediaSource {
ssrc: number;
attribute?: string;
value?: string;
}
export interface SDPMediaSourceGroup {
semantics: string;
ssrcs: number[];
}
export interface SDPMediaStreamId {
stream: string;
track: string;
}
export interface SDPCodec extends SDPCodecParameters {
payloadType: number;
preferredPayloadType?: number;
parameters?: SDPCodecAdditionalParameters;
rtcpFeedback?: SDPFeedbackParameter[];
}
export interface SDPGroup {
semantics: string;
mids: string[];
}
export interface SDPMLine {
kind: string;
port?: number;
protocol: string;
fmt?: string;
}
export interface SDPOLine {
username: string;
sessionId: string;
sessionVersion: number;
netType: string;
addressType: string;
address: string;
}
export interface SDPRtcpParameters {
cname?: string;
ssrc?: number;
reducedSize?: boolean;
compound?: boolean;
mux?: boolean;
}
export interface SDPEncodingParameters {
ssrc: number;
codecPayloadType?: number;
rtx?: {
ssrc: number;
};
fec?: {
ssrc: number;
mechanism: string;
};
}
export interface SDPRtpCapabilities {
codecs: SDPCodec[];
headerExtensions: SDPHeaderExtension[];
fecMechanisms: string[];
rtcp?: SDPRtcpParameters[];
}
export interface SDPSctpDescription {
port: number;
protocol: string;
maxMessageSize?: number;
}
export const localCname: string;
export function generateIdentifier(): string;
export function splitLines(blob: SDPBlob): SDPLine[];
export function splitSections(blob: SDPBlob): SDPSection[];
export function getDescription(blob: SDPBlob): SDPSection;
export function getMediaSections(blob: SDPBlob): SDPSection[];
export function matchPrefix(blob: SDPBlob, prefix: string): SDPLine[];
export function parseCandidate(line: SDPLine): SDPIceCandidate;
export function writeCandidate(candidate: SDPIceCandidate): SDPLine;
export function parseIceOptions(line: SDPLine): string[];
export function parseRtpMap(line: SDPLine): SDPCodecParameters;
export function writeRtpMap(codec: SDPCodecParameters): SDPLine;
export function parseExtmap(line: SDPLine): SDPHeaderExtension;
export function writeExtmap(headerExtension: SDPHeaderExtension): SDPLine;
export function parseFmtp(line: SDPLine): SDPCodecAdditionalParameters;
export function writeFmtp(codec: SDPCodec): SDPLine;
export function parseRtcpFb(line: SDPLine): SDPFeedbackParameter;
export function writeRtcpFb(codec: SDPCodec): SDPLine[];
export function parseSsrcMedia(line: SDPLine): SDPMediaSource;
export function parseSsrcGroup(line: SDPLine): SDPMediaSourceGroup;
export function getMid(mediaSection: SDPSection): string;
export function parseFingerprint(line: SDPLine): SDPFingerprint;
export function getDtlsParameters(
mediaSection: SDPSection,
session: SDPSection
): SDPDtlsParameters;
export function writeDtlsParameters(params: SDPDtlsParameters, setupType: string): SDPLine;
export function getIceParameters(
mediaSection: SDPSection,
session: SDPSection
): SDPIceParameters;
export function writeIceParameters(params: SDPIceParameters): SDPLine;
export function parseRtpParameters(mediaSection: SDPSection): SDPRtpCapabilities;
export function writeRtpDescription(kind: string, caps: SDPRtpCapabilities): SDPSection;
export function parseRtpEncodingParameters(mediaSection: SDPSection): SDPEncodingParameters[];
export function parseRtcpParameters(mediaSection: SDPSection): SDPRtcpParameters;
export function writeRtcpParameters(params: SDPRtcpParameters): SDPLine;
export function parseMsid(mediaSection: SDPSection): SDPMediaStreamId;
export function parseSctpDescription(mediaSection: SDPSection): SDPSctpDescription;
export function writeSctpDescription(
mediaSection: SDPMLine,
desc: SDPSctpDescription
): SDPSection;
export function generateSessionId(): string;
export function writeSessionBoilerplate(
sessId?: string,
sessVer?: number,
sessUser?: string
): SDPBlob;
export function getDirection(mediaSection: SDPSection, sessionpart: SDPSection): SDPDirection;
export function getKind(mediaSection: SDPSection): string;
export function isRejected(mediaSection: SDPSection): boolean;
export function parseMLine(mediaSection: SDPSection): SDPMLine;
export function parseOLine(mediaSection: SDPSection): SDPOLine;
export function isValidSDP(blob: SDPBlob): boolean;
Generated Vendored
+802
View File
@@ -0,0 +1,802 @@
/* eslint-env node */
'use strict';
// SDP helpers.
const SDPUtils = {};
// Generate an alphanumeric identifier for cname or mids.
// TODO: use UUIDs instead? https://gist.github.com/jed/982883
SDPUtils.generateIdentifier = function() {
return Math.random().toString(36).substring(2, 12);
};
// The RTCP CNAME used by all peerconnections from the same JS.
SDPUtils.localCName = SDPUtils.generateIdentifier();
// Splits SDP into lines, dealing with both CRLF and LF.
SDPUtils.splitLines = function(blob) {
return blob.trim().split('\n').map(line => line.trim());
};
// Splits SDP into sessionpart and mediasections. Ensures CRLF.
SDPUtils.splitSections = function(blob) {
const parts = blob.split('\nm=');
return parts.map((part, index) => (index > 0 ?
'm=' + part : part).trim() + '\r\n');
};
// Returns the session description.
SDPUtils.getDescription = function(blob) {
const sections = SDPUtils.splitSections(blob);
return sections && sections[0];
};
// Returns the individual media sections.
SDPUtils.getMediaSections = function(blob) {
const sections = SDPUtils.splitSections(blob);
sections.shift();
return sections;
};
// Returns lines that start with a certain prefix.
SDPUtils.matchPrefix = function(blob, prefix) {
return SDPUtils.splitLines(blob).filter(line => line.indexOf(prefix) === 0);
};
// Parses an ICE candidate line. Sample input:
// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
// rport 55996"
// Input can be prefixed with a=.
SDPUtils.parseCandidate = function(line) {
let parts;
// Parse both variants.
if (line.indexOf('a=candidate:') === 0) {
parts = line.substring(12).split(' ');
} else {
parts = line.substring(10).split(' ');
}
const candidate = {
foundation: parts[0],
component: {1: 'rtp', 2: 'rtcp'}[parts[1]] || parts[1],
protocol: parts[2].toLowerCase(),
priority: parseInt(parts[3], 10),
ip: parts[4],
address: parts[4], // address is an alias for ip.
port: parseInt(parts[5], 10),
// skip parts[6] == 'typ'
type: parts[7],
};
for (let i = 8; i < parts.length; i += 2) {
switch (parts[i]) {
case 'raddr':
candidate.relatedAddress = parts[i + 1];
break;
case 'rport':
candidate.relatedPort = parseInt(parts[i + 1], 10);
break;
case 'tcptype':
candidate.tcpType = parts[i + 1];
break;
case 'ufrag':
candidate.ufrag = parts[i + 1]; // for backward compatibility.
candidate.usernameFragment = parts[i + 1];
break;
default: // extension handling, in particular ufrag. Don't overwrite.
if (candidate[parts[i]] === undefined) {
candidate[parts[i]] = parts[i + 1];
}
break;
}
}
return candidate;
};
// Translates a candidate object into SDP candidate attribute.
// This does not include the a= prefix!
SDPUtils.writeCandidate = function(candidate) {
const sdp = [];
sdp.push(candidate.foundation);
const component = candidate.component;
if (component === 'rtp') {
sdp.push(1);
} else if (component === 'rtcp') {
sdp.push(2);
} else {
sdp.push(component);
}
sdp.push(candidate.protocol.toUpperCase());
sdp.push(candidate.priority);
sdp.push(candidate.address || candidate.ip);
sdp.push(candidate.port);
const type = candidate.type;
sdp.push('typ');
sdp.push(type);
if (type !== 'host' && candidate.relatedAddress &&
candidate.relatedPort) {
sdp.push('raddr');
sdp.push(candidate.relatedAddress);
sdp.push('rport');
sdp.push(candidate.relatedPort);
}
if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
sdp.push('tcptype');
sdp.push(candidate.tcpType);
}
if (candidate.usernameFragment || candidate.ufrag) {
sdp.push('ufrag');
sdp.push(candidate.usernameFragment || candidate.ufrag);
}
return 'candidate:' + sdp.join(' ');
};
// Parses an ice-options line, returns an array of option tags.
// Sample input:
// a=ice-options:foo bar
SDPUtils.parseIceOptions = function(line) {
return line.substring(14).split(' ');
};
// Parses a rtpmap line, returns RTCRtpCoddecParameters. Sample input:
// a=rtpmap:111 opus/48000/2
SDPUtils.parseRtpMap = function(line) {
let parts = line.substring(9).split(' ');
const parsed = {
payloadType: parseInt(parts.shift(), 10), // was: id
};
parts = parts[0].split('/');
parsed.name = parts[0];
parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
// legacy alias, got renamed back to channels in ORTC.
parsed.numChannels = parsed.channels;
return parsed;
};
// Generates a rtpmap line from RTCRtpCodecCapability or
// RTCRtpCodecParameters.
SDPUtils.writeRtpMap = function(codec) {
let pt = codec.payloadType;
if (codec.preferredPayloadType !== undefined) {
pt = codec.preferredPayloadType;
}
const channels = codec.channels || codec.numChannels || 1;
return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
(channels !== 1 ? '/' + channels : '') + '\r\n';
};
// Parses a extmap line (headerextension from RFC 5285). Sample input:
// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
SDPUtils.parseExtmap = function(line) {
const parts = line.substring(9).split(' ');
return {
id: parseInt(parts[0], 10),
direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
uri: parts[1],
attributes: parts.slice(2).join(' '),
};
};
// Generates an extmap line from RTCRtpHeaderExtensionParameters or
// RTCRtpHeaderExtension.
SDPUtils.writeExtmap = function(headerExtension) {
return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
(headerExtension.direction && headerExtension.direction !== 'sendrecv'
? '/' + headerExtension.direction
: '') +
' ' + headerExtension.uri +
(headerExtension.attributes ? ' ' + headerExtension.attributes : '') +
'\r\n';
};
// Parses a fmtp line, returns dictionary. Sample input:
// a=fmtp:96 vbr=on;cng=on
// Also deals with vbr=on; cng=on
SDPUtils.parseFmtp = function(line) {
const parsed = {};
let kv;
const parts = line.substring(line.indexOf(' ') + 1).split(';');
for (let j = 0; j < parts.length; j++) {
kv = parts[j].trim().split('=');
parsed[kv[0].trim()] = kv[1];
}
return parsed;
};
// Generates a fmtp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
SDPUtils.writeFmtp = function(codec) {
let line = '';
let pt = codec.payloadType;
if (codec.preferredPayloadType !== undefined) {
pt = codec.preferredPayloadType;
}
if (codec.parameters && Object.keys(codec.parameters).length) {
const params = [];
Object.keys(codec.parameters).forEach(param => {
if (codec.parameters[param] !== undefined) {
params.push(param + '=' + codec.parameters[param]);
} else {
params.push(param);
}
});
line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
}
return line;
};
// Parses a rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
// a=rtcp-fb:98 nack rpsi
SDPUtils.parseRtcpFb = function(line) {
const parts = line.substring(line.indexOf(' ') + 1).split(' ');
return {
type: parts.shift(),
parameter: parts.join(' '),
};
};
// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
SDPUtils.writeRtcpFb = function(codec) {
let lines = '';
let pt = codec.payloadType;
if (codec.preferredPayloadType !== undefined) {
pt = codec.preferredPayloadType;
}
if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
// FIXME: special handling for trr-int?
codec.rtcpFeedback.forEach(fb => {
lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
(fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
'\r\n';
});
}
return lines;
};
// Parses a RFC 5576 ssrc media attribute. Sample input:
// a=ssrc:3735928559 cname:something
SDPUtils.parseSsrcMedia = function(line) {
const sp = line.indexOf(' ');
const parts = {
ssrc: parseInt(line.substring(7, sp), 10),
};
const colon = line.indexOf(':', sp);
if (colon > -1) {
parts.attribute = line.substring(sp + 1, colon);
parts.value = line.substring(colon + 1);
} else {
parts.attribute = line.substring(sp + 1);
}
return parts;
};
// Parse a ssrc-group line (see RFC 5576). Sample input:
// a=ssrc-group:semantics 12 34
SDPUtils.parseSsrcGroup = function(line) {
const parts = line.substring(13).split(' ');
return {
semantics: parts.shift(),
ssrcs: parts.map(ssrc => parseInt(ssrc, 10)),
};
};
// Extracts the MID (RFC 5888) from a media section.
// Returns the MID or undefined if no mid line was found.
SDPUtils.getMid = function(mediaSection) {
const mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
if (mid) {
return mid.substring(6);
}
};
// Parses a fingerprint line for DTLS-SRTP.
SDPUtils.parseFingerprint = function(line) {
const parts = line.substring(14).split(' ');
return {
algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
value: parts[1].toUpperCase(), // the definition is upper-case in RFC 4572.
};
};
// Extracts DTLS parameters from SDP media section or sessionpart.
// FIXME: for consistency with other functions this should only
// get the fingerprint line as input. See also getIceParameters.
SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
const lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
'a=fingerprint:');
// Note: a=setup line is ignored since we use the 'auto' role in Edge.
return {
role: 'auto',
fingerprints: lines.map(SDPUtils.parseFingerprint),
};
};
// Serializes DTLS parameters to SDP.
SDPUtils.writeDtlsParameters = function(params, setupType) {
let sdp = 'a=setup:' + setupType + '\r\n';
params.fingerprints.forEach(fp => {
sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
});
return sdp;
};
// Parses a=crypto lines into
// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members
SDPUtils.parseCryptoLine = function(line) {
const parts = line.substring(9).split(' ');
return {
tag: parseInt(parts[0], 10),
cryptoSuite: parts[1],
keyParams: parts[2],
sessionParams: parts.slice(3),
};
};
SDPUtils.writeCryptoLine = function(parameters) {
return 'a=crypto:' + parameters.tag + ' ' +
parameters.cryptoSuite + ' ' +
(typeof parameters.keyParams === 'object'
? SDPUtils.writeCryptoKeyParams(parameters.keyParams)
: parameters.keyParams) +
(parameters.sessionParams ? ' ' + parameters.sessionParams.join(' ') : '') +
'\r\n';
};
// Parses the crypto key parameters into
// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam*
SDPUtils.parseCryptoKeyParams = function(keyParams) {
if (keyParams.indexOf('inline:') !== 0) {
return null;
}
const parts = keyParams.substring(7).split('|');
return {
keyMethod: 'inline',
keySalt: parts[0],
lifeTime: parts[1],
mkiValue: parts[2] ? parts[2].split(':')[0] : undefined,
mkiLength: parts[2] ? parts[2].split(':')[1] : undefined,
};
};
SDPUtils.writeCryptoKeyParams = function(keyParams) {
return keyParams.keyMethod + ':'
+ keyParams.keySalt +
(keyParams.lifeTime ? '|' + keyParams.lifeTime : '') +
(keyParams.mkiValue && keyParams.mkiLength
? '|' + keyParams.mkiValue + ':' + keyParams.mkiLength
: '');
};
// Extracts all SDES parameters.
SDPUtils.getCryptoParameters = function(mediaSection, sessionpart) {
const lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
'a=crypto:');
return lines.map(SDPUtils.parseCryptoLine);
};
// Parses ICE information from SDP media section or sessionpart.
// FIXME: for consistency with other functions this should only
// get the ice-ufrag and ice-pwd lines as input.
SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
const ufrag = SDPUtils.matchPrefix(mediaSection + sessionpart,
'a=ice-ufrag:')[0];
const pwd = SDPUtils.matchPrefix(mediaSection + sessionpart,
'a=ice-pwd:')[0];
if (!(ufrag && pwd)) {
return null;
}
return {
usernameFragment: ufrag.substring(12),
password: pwd.substring(10),
};
};
// Serializes ICE parameters to SDP.
SDPUtils.writeIceParameters = function(params) {
let sdp = 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
'a=ice-pwd:' + params.password + '\r\n';
if (params.iceLite) {
sdp += 'a=ice-lite\r\n';
}
return sdp;
};
// Parses the SDP media section and returns RTCRtpParameters.
SDPUtils.parseRtpParameters = function(mediaSection) {
const description = {
codecs: [],
headerExtensions: [],
fecMechanisms: [],
rtcp: [],
};
const lines = SDPUtils.splitLines(mediaSection);
const mline = lines[0].split(' ');
description.profile = mline[2];
for (let i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
const pt = mline[i];
const rtpmapline = SDPUtils.matchPrefix(
mediaSection, 'a=rtpmap:' + pt + ' ')[0];
if (rtpmapline) {
const codec = SDPUtils.parseRtpMap(rtpmapline);
const fmtps = SDPUtils.matchPrefix(
mediaSection, 'a=fmtp:' + pt + ' ');
// Only the first a=fmtp:<pt> is considered.
codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
codec.rtcpFeedback = SDPUtils.matchPrefix(
mediaSection, 'a=rtcp-fb:' + pt + ' ')
.map(SDPUtils.parseRtcpFb);
description.codecs.push(codec);
// parse FEC mechanisms from rtpmap lines.
switch (codec.name.toUpperCase()) {
case 'RED':
case 'ULPFEC':
description.fecMechanisms.push(codec.name.toUpperCase());
break;
default: // only RED and ULPFEC are recognized as FEC mechanisms.
break;
}
}
}
SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(line => {
description.headerExtensions.push(SDPUtils.parseExtmap(line));
});
const wildcardRtcpFb = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:* ')
.map(SDPUtils.parseRtcpFb);
description.codecs.forEach(codec => {
wildcardRtcpFb.forEach(fb=> {
const duplicate = codec.rtcpFeedback.find(existingFeedback => {
return existingFeedback.type === fb.type &&
existingFeedback.parameter === fb.parameter;
});
if (!duplicate) {
codec.rtcpFeedback.push(fb);
}
});
});
// FIXME: parse rtcp.
return description;
};
// Generates parts of the SDP media section describing the capabilities /
// parameters.
SDPUtils.writeRtpDescription = function(kind, caps) {
let sdp = '';
// Build the mline.
sdp += 'm=' + kind + ' ';
sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
sdp += ' ' + (caps.profile || 'UDP/TLS/RTP/SAVPF') + ' ';
sdp += caps.codecs.map(codec => {
if (codec.preferredPayloadType !== undefined) {
return codec.preferredPayloadType;
}
return codec.payloadType;
}).join(' ') + '\r\n';
sdp += 'c=IN IP4 0.0.0.0\r\n';
sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
// Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
caps.codecs.forEach(codec => {
sdp += SDPUtils.writeRtpMap(codec);
sdp += SDPUtils.writeFmtp(codec);
sdp += SDPUtils.writeRtcpFb(codec);
});
let maxptime = 0;
caps.codecs.forEach(codec => {
if (codec.maxptime > maxptime) {
maxptime = codec.maxptime;
}
});
if (maxptime > 0) {
sdp += 'a=maxptime:' + maxptime + '\r\n';
}
if (caps.headerExtensions) {
caps.headerExtensions.forEach(extension => {
sdp += SDPUtils.writeExtmap(extension);
});
}
// FIXME: write fecMechanisms.
return sdp;
};
// Parses the SDP media section and returns an array of
// RTCRtpEncodingParameters.
SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
const encodingParameters = [];
const description = SDPUtils.parseRtpParameters(mediaSection);
const hasRed = description.fecMechanisms.indexOf('RED') !== -1;
const hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
// filter a=ssrc:... cname:, ignore PlanB-msid
const ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
.map(line => SDPUtils.parseSsrcMedia(line))
.filter(parts => parts.attribute === 'cname');
const primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
let secondarySsrc;
const flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
.map(line => {
const parts = line.substring(17).split(' ');
return parts.map(part => parseInt(part, 10));
});
if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
secondarySsrc = flows[0][1];
}
description.codecs.forEach(codec => {
if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
let encParam = {
ssrc: primarySsrc,
codecPayloadType: parseInt(codec.parameters.apt, 10),
};
if (primarySsrc && secondarySsrc) {
encParam.rtx = {ssrc: secondarySsrc};
}
encodingParameters.push(encParam);
if (hasRed) {
encParam = JSON.parse(JSON.stringify(encParam));
encParam.fec = {
ssrc: primarySsrc,
mechanism: hasUlpfec ? 'red+ulpfec' : 'red',
};
encodingParameters.push(encParam);
}
}
});
if (encodingParameters.length === 0 && primarySsrc) {
encodingParameters.push({
ssrc: primarySsrc,
});
}
// we support both b=AS and b=TIAS but interpret AS as TIAS.
let bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
if (bandwidth.length) {
if (bandwidth[0].indexOf('b=TIAS:') === 0) {
bandwidth = parseInt(bandwidth[0].substring(7), 10);
} else if (bandwidth[0].indexOf('b=AS:') === 0) {
// use formula from JSEP to convert b=AS to TIAS value.
bandwidth = parseInt(bandwidth[0].substring(5), 10) * 1000 * 0.95
- (50 * 40 * 8);
} else {
bandwidth = undefined;
}
encodingParameters.forEach(params => {
params.maxBitrate = bandwidth;
});
}
return encodingParameters;
};
// parses http://draft.ortc.org/#rtcrtcpparameters*
SDPUtils.parseRtcpParameters = function(mediaSection) {
const rtcpParameters = {};
// Gets the first SSRC. Note that with RTX there might be multiple
// SSRCs.
const remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
.map(line => SDPUtils.parseSsrcMedia(line))
.filter(obj => obj.attribute === 'cname')[0];
if (remoteSsrc) {
rtcpParameters.cname = remoteSsrc.value;
rtcpParameters.ssrc = remoteSsrc.ssrc;
}
// Edge uses the compound attribute instead of reducedSize
// compound is !reducedSize
const rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
rtcpParameters.reducedSize = rsize.length > 0;
rtcpParameters.compound = rsize.length === 0;
// parses the rtcp-mux attrіbute.
// Note that Edge does not support unmuxed RTCP.
const mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
rtcpParameters.mux = mux.length > 0;
return rtcpParameters;
};
SDPUtils.writeRtcpParameters = function(rtcpParameters) {
let sdp = '';
if (rtcpParameters.reducedSize) {
sdp += 'a=rtcp-rsize\r\n';
}
if (rtcpParameters.mux) {
sdp += 'a=rtcp-mux\r\n';
}
if (rtcpParameters.ssrc !== undefined && rtcpParameters.cname) {
sdp += 'a=ssrc:' + rtcpParameters.ssrc +
' cname:' + rtcpParameters.cname + '\r\n';
}
return sdp;
};
// parses either a=msid: or a=ssrc:... msid lines and returns
// the id of the MediaStream and MediaStreamTrack.
SDPUtils.parseMsid = function(mediaSection) {
let parts;
const spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
if (spec.length === 1) {
parts = spec[0].substring(7).split(' ');
return {stream: parts[0], track: parts[1]};
}
const planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
.map(line => SDPUtils.parseSsrcMedia(line))
.filter(msidParts => msidParts.attribute === 'msid');
if (planB.length > 0) {
parts = planB[0].value.split(' ');
return {stream: parts[0], track: parts[1]};
}
};
// SCTP
// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back
// to draft-ietf-mmusic-sctp-sdp-05
SDPUtils.parseSctpDescription = function(mediaSection) {
const mline = SDPUtils.parseMLine(mediaSection);
const maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:');
let maxMessageSize;
if (maxSizeLine.length > 0) {
maxMessageSize = parseInt(maxSizeLine[0].substring(19), 10);
}
if (isNaN(maxMessageSize)) {
maxMessageSize = 65536;
}
const sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:');
if (sctpPort.length > 0) {
return {
port: parseInt(sctpPort[0].substring(12), 10),
protocol: mline.fmt,
maxMessageSize,
};
}
const sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:');
if (sctpMapLines.length > 0) {
const parts = sctpMapLines[0]
.substring(10)
.split(' ');
return {
port: parseInt(parts[0], 10),
protocol: parts[1],
maxMessageSize,
};
}
};
// SCTP
// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers
// support by now receiving in this format, unless we originally parsed
// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line
// protocol of DTLS/SCTP -- without UDP/ or TCP/)
SDPUtils.writeSctpDescription = function(media, sctp) {
let output = [];
if (media.protocol !== 'DTLS/SCTP') {
output = [
'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n',
'c=IN IP4 0.0.0.0\r\n',
'a=sctp-port:' + sctp.port + '\r\n',
];
} else {
output = [
'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n',
'c=IN IP4 0.0.0.0\r\n',
'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n',
];
}
if (sctp.maxMessageSize !== undefined) {
output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n');
}
return output.join('');
};
// Generate a session ID for SDP.
// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
// recommends using a cryptographically random +ve 64-bit value
// but right now this should be acceptable and within the right range
SDPUtils.generateSessionId = function() {
return Math.random().toString().substr(2, 22);
};
// Write boiler plate for start of SDP
// sessId argument is optional - if not supplied it will
// be generated randomly
// sessVersion is optional and defaults to 2
// sessUser is optional and defaults to 'thisisadapterortc'
SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) {
let sessionId;
const version = sessVer !== undefined ? sessVer : 2;
if (sessId) {
sessionId = sessId;
} else {
sessionId = SDPUtils.generateSessionId();
}
const user = sessUser || 'thisisadapterortc';
// FIXME: sess-id should be an NTP timestamp.
return 'v=0\r\n' +
'o=' + user + ' ' + sessionId + ' ' + version +
' IN IP4 127.0.0.1\r\n' +
's=-\r\n' +
't=0 0\r\n';
};
// Gets the direction from the mediaSection or the sessionpart.
SDPUtils.getDirection = function(mediaSection, sessionpart) {
// Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
const lines = SDPUtils.splitLines(mediaSection);
for (let i = 0; i < lines.length; i++) {
switch (lines[i]) {
case 'a=sendrecv':
case 'a=sendonly':
case 'a=recvonly':
case 'a=inactive':
return lines[i].substring(2);
default:
// FIXME: What should happen here?
}
}
if (sessionpart) {
return SDPUtils.getDirection(sessionpart);
}
return 'sendrecv';
};
SDPUtils.getKind = function(mediaSection) {
const lines = SDPUtils.splitLines(mediaSection);
const mline = lines[0].split(' ');
return mline[0].substring(2);
};
SDPUtils.isRejected = function(mediaSection) {
return mediaSection.split(' ', 2)[1] === '0';
};
SDPUtils.parseMLine = function(mediaSection) {
const lines = SDPUtils.splitLines(mediaSection);
const parts = lines[0].substring(2).split(' ');
return {
kind: parts[0],
port: parseInt(parts[1], 10),
protocol: parts[2],
fmt: parts.slice(3).join(' '),
};
};
SDPUtils.parseOLine = function(mediaSection) {
const line = SDPUtils.matchPrefix(mediaSection, 'o=')[0];
const parts = line.substring(2).split(' ');
return {
username: parts[0],
sessionId: parts[1],
sessionVersion: parseInt(parts[2], 10),
netType: parts[3],
addressType: parts[4],
address: parts[5],
};
};
// a very naive interpretation of a valid SDP.
SDPUtils.isValidSDP = function(blob) {
if (typeof blob !== 'string' || blob.length === 0) {
return false;
}
const lines = SDPUtils.splitLines(blob);
for (let i = 0; i < lines.length; i++) {
if (lines[i].length < 2 || lines[i].charAt(1) !== '=') {
return false;
}
// TODO: check the modifier a bit more.
}
return true;
};
// Expose public methods.
if (typeof module === 'object') {
module.exports = SDPUtils;
}