sdp.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  1. /* eslint-env node */
  2. 'use strict';
  3. // SDP helpers.
  4. const SDPUtils = {};
  5. // Generate an alphanumeric identifier for cname or mids.
  6. // TODO: use UUIDs instead? https://gist.github.com/jed/982883
  7. SDPUtils.generateIdentifier = function() {
  8. return Math.random().toString(36).substring(2, 12);
  9. };
  10. // The RTCP CNAME used by all peerconnections from the same JS.
  11. SDPUtils.localCName = SDPUtils.generateIdentifier();
  12. // Splits SDP into lines, dealing with both CRLF and LF.
  13. SDPUtils.splitLines = function(blob) {
  14. return blob.trim().split('\n').map(line => line.trim());
  15. };
  16. // Splits SDP into sessionpart and mediasections. Ensures CRLF.
  17. SDPUtils.splitSections = function(blob) {
  18. const parts = blob.split('\nm=');
  19. return parts.map((part, index) => (index > 0 ?
  20. 'm=' + part : part).trim() + '\r\n');
  21. };
  22. // Returns the session description.
  23. SDPUtils.getDescription = function(blob) {
  24. const sections = SDPUtils.splitSections(blob);
  25. return sections && sections[0];
  26. };
  27. // Returns the individual media sections.
  28. SDPUtils.getMediaSections = function(blob) {
  29. const sections = SDPUtils.splitSections(blob);
  30. sections.shift();
  31. return sections;
  32. };
  33. // Returns lines that start with a certain prefix.
  34. SDPUtils.matchPrefix = function(blob, prefix) {
  35. return SDPUtils.splitLines(blob).filter(line => line.indexOf(prefix) === 0);
  36. };
  37. // Parses an ICE candidate line. Sample input:
  38. // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
  39. // rport 55996"
  40. // Input can be prefixed with a=.
  41. SDPUtils.parseCandidate = function(line) {
  42. let parts;
  43. // Parse both variants.
  44. if (line.indexOf('a=candidate:') === 0) {
  45. parts = line.substring(12).split(' ');
  46. } else {
  47. parts = line.substring(10).split(' ');
  48. }
  49. const candidate = {
  50. foundation: parts[0],
  51. component: {1: 'rtp', 2: 'rtcp'}[parts[1]] || parts[1],
  52. protocol: parts[2].toLowerCase(),
  53. priority: parseInt(parts[3], 10),
  54. ip: parts[4],
  55. address: parts[4], // address is an alias for ip.
  56. port: parseInt(parts[5], 10),
  57. // skip parts[6] == 'typ'
  58. type: parts[7],
  59. };
  60. for (let i = 8; i < parts.length; i += 2) {
  61. switch (parts[i]) {
  62. case 'raddr':
  63. candidate.relatedAddress = parts[i + 1];
  64. break;
  65. case 'rport':
  66. candidate.relatedPort = parseInt(parts[i + 1], 10);
  67. break;
  68. case 'tcptype':
  69. candidate.tcpType = parts[i + 1];
  70. break;
  71. case 'ufrag':
  72. candidate.ufrag = parts[i + 1]; // for backward compatibility.
  73. candidate.usernameFragment = parts[i + 1];
  74. break;
  75. default: // extension handling, in particular ufrag. Don't overwrite.
  76. if (candidate[parts[i]] === undefined) {
  77. candidate[parts[i]] = parts[i + 1];
  78. }
  79. break;
  80. }
  81. }
  82. return candidate;
  83. };
  84. // Translates a candidate object into SDP candidate attribute.
  85. // This does not include the a= prefix!
  86. SDPUtils.writeCandidate = function(candidate) {
  87. const sdp = [];
  88. sdp.push(candidate.foundation);
  89. const component = candidate.component;
  90. if (component === 'rtp') {
  91. sdp.push(1);
  92. } else if (component === 'rtcp') {
  93. sdp.push(2);
  94. } else {
  95. sdp.push(component);
  96. }
  97. sdp.push(candidate.protocol.toUpperCase());
  98. sdp.push(candidate.priority);
  99. sdp.push(candidate.address || candidate.ip);
  100. sdp.push(candidate.port);
  101. const type = candidate.type;
  102. sdp.push('typ');
  103. sdp.push(type);
  104. if (type !== 'host' && candidate.relatedAddress &&
  105. candidate.relatedPort) {
  106. sdp.push('raddr');
  107. sdp.push(candidate.relatedAddress);
  108. sdp.push('rport');
  109. sdp.push(candidate.relatedPort);
  110. }
  111. if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
  112. sdp.push('tcptype');
  113. sdp.push(candidate.tcpType);
  114. }
  115. if (candidate.usernameFragment || candidate.ufrag) {
  116. sdp.push('ufrag');
  117. sdp.push(candidate.usernameFragment || candidate.ufrag);
  118. }
  119. return 'candidate:' + sdp.join(' ');
  120. };
  121. // Parses an ice-options line, returns an array of option tags.
  122. // Sample input:
  123. // a=ice-options:foo bar
  124. SDPUtils.parseIceOptions = function(line) {
  125. return line.substring(14).split(' ');
  126. };
  127. // Parses a rtpmap line, returns RTCRtpCoddecParameters. Sample input:
  128. // a=rtpmap:111 opus/48000/2
  129. SDPUtils.parseRtpMap = function(line) {
  130. let parts = line.substring(9).split(' ');
  131. const parsed = {
  132. payloadType: parseInt(parts.shift(), 10), // was: id
  133. };
  134. parts = parts[0].split('/');
  135. parsed.name = parts[0];
  136. parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
  137. parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
  138. // legacy alias, got renamed back to channels in ORTC.
  139. parsed.numChannels = parsed.channels;
  140. return parsed;
  141. };
  142. // Generates a rtpmap line from RTCRtpCodecCapability or
  143. // RTCRtpCodecParameters.
  144. SDPUtils.writeRtpMap = function(codec) {
  145. let pt = codec.payloadType;
  146. if (codec.preferredPayloadType !== undefined) {
  147. pt = codec.preferredPayloadType;
  148. }
  149. const channels = codec.channels || codec.numChannels || 1;
  150. return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
  151. (channels !== 1 ? '/' + channels : '') + '\r\n';
  152. };
  153. // Parses a extmap line (headerextension from RFC 5285). Sample input:
  154. // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
  155. // a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
  156. SDPUtils.parseExtmap = function(line) {
  157. const parts = line.substring(9).split(' ');
  158. return {
  159. id: parseInt(parts[0], 10),
  160. direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
  161. uri: parts[1],
  162. attributes: parts.slice(2).join(' '),
  163. };
  164. };
  165. // Generates an extmap line from RTCRtpHeaderExtensionParameters or
  166. // RTCRtpHeaderExtension.
  167. SDPUtils.writeExtmap = function(headerExtension) {
  168. return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
  169. (headerExtension.direction && headerExtension.direction !== 'sendrecv'
  170. ? '/' + headerExtension.direction
  171. : '') +
  172. ' ' + headerExtension.uri +
  173. (headerExtension.attributes ? ' ' + headerExtension.attributes : '') +
  174. '\r\n';
  175. };
  176. // Parses a fmtp line, returns dictionary. Sample input:
  177. // a=fmtp:96 vbr=on;cng=on
  178. // Also deals with vbr=on; cng=on
  179. SDPUtils.parseFmtp = function(line) {
  180. const parsed = {};
  181. let kv;
  182. const parts = line.substring(line.indexOf(' ') + 1).split(';');
  183. for (let j = 0; j < parts.length; j++) {
  184. kv = parts[j].trim().split('=');
  185. parsed[kv[0].trim()] = kv[1];
  186. }
  187. return parsed;
  188. };
  189. // Generates a fmtp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
  190. SDPUtils.writeFmtp = function(codec) {
  191. let line = '';
  192. let pt = codec.payloadType;
  193. if (codec.preferredPayloadType !== undefined) {
  194. pt = codec.preferredPayloadType;
  195. }
  196. if (codec.parameters && Object.keys(codec.parameters).length) {
  197. const params = [];
  198. Object.keys(codec.parameters).forEach(param => {
  199. if (codec.parameters[param] !== undefined) {
  200. params.push(param + '=' + codec.parameters[param]);
  201. } else {
  202. params.push(param);
  203. }
  204. });
  205. line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
  206. }
  207. return line;
  208. };
  209. // Parses a rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
  210. // a=rtcp-fb:98 nack rpsi
  211. SDPUtils.parseRtcpFb = function(line) {
  212. const parts = line.substring(line.indexOf(' ') + 1).split(' ');
  213. return {
  214. type: parts.shift(),
  215. parameter: parts.join(' '),
  216. };
  217. };
  218. // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
  219. SDPUtils.writeRtcpFb = function(codec) {
  220. let lines = '';
  221. let pt = codec.payloadType;
  222. if (codec.preferredPayloadType !== undefined) {
  223. pt = codec.preferredPayloadType;
  224. }
  225. if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
  226. // FIXME: special handling for trr-int?
  227. codec.rtcpFeedback.forEach(fb => {
  228. lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
  229. (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
  230. '\r\n';
  231. });
  232. }
  233. return lines;
  234. };
  235. // Parses a RFC 5576 ssrc media attribute. Sample input:
  236. // a=ssrc:3735928559 cname:something
  237. SDPUtils.parseSsrcMedia = function(line) {
  238. const sp = line.indexOf(' ');
  239. const parts = {
  240. ssrc: parseInt(line.substring(7, sp), 10),
  241. };
  242. const colon = line.indexOf(':', sp);
  243. if (colon > -1) {
  244. parts.attribute = line.substring(sp + 1, colon);
  245. parts.value = line.substring(colon + 1);
  246. } else {
  247. parts.attribute = line.substring(sp + 1);
  248. }
  249. return parts;
  250. };
  251. // Parse a ssrc-group line (see RFC 5576). Sample input:
  252. // a=ssrc-group:semantics 12 34
  253. SDPUtils.parseSsrcGroup = function(line) {
  254. const parts = line.substring(13).split(' ');
  255. return {
  256. semantics: parts.shift(),
  257. ssrcs: parts.map(ssrc => parseInt(ssrc, 10)),
  258. };
  259. };
  260. // Extracts the MID (RFC 5888) from a media section.
  261. // Returns the MID or undefined if no mid line was found.
  262. SDPUtils.getMid = function(mediaSection) {
  263. const mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
  264. if (mid) {
  265. return mid.substring(6);
  266. }
  267. };
  268. // Parses a fingerprint line for DTLS-SRTP.
  269. SDPUtils.parseFingerprint = function(line) {
  270. const parts = line.substring(14).split(' ');
  271. return {
  272. algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
  273. value: parts[1].toUpperCase(), // the definition is upper-case in RFC 4572.
  274. };
  275. };
  276. // Extracts DTLS parameters from SDP media section or sessionpart.
  277. // FIXME: for consistency with other functions this should only
  278. // get the fingerprint line as input. See also getIceParameters.
  279. SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
  280. const lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
  281. 'a=fingerprint:');
  282. // Note: a=setup line is ignored since we use the 'auto' role in Edge.
  283. return {
  284. role: 'auto',
  285. fingerprints: lines.map(SDPUtils.parseFingerprint),
  286. };
  287. };
  288. // Serializes DTLS parameters to SDP.
  289. SDPUtils.writeDtlsParameters = function(params, setupType) {
  290. let sdp = 'a=setup:' + setupType + '\r\n';
  291. params.fingerprints.forEach(fp => {
  292. sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
  293. });
  294. return sdp;
  295. };
  296. // Parses a=crypto lines into
  297. // https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members
  298. SDPUtils.parseCryptoLine = function(line) {
  299. const parts = line.substring(9).split(' ');
  300. return {
  301. tag: parseInt(parts[0], 10),
  302. cryptoSuite: parts[1],
  303. keyParams: parts[2],
  304. sessionParams: parts.slice(3),
  305. };
  306. };
  307. SDPUtils.writeCryptoLine = function(parameters) {
  308. return 'a=crypto:' + parameters.tag + ' ' +
  309. parameters.cryptoSuite + ' ' +
  310. (typeof parameters.keyParams === 'object'
  311. ? SDPUtils.writeCryptoKeyParams(parameters.keyParams)
  312. : parameters.keyParams) +
  313. (parameters.sessionParams ? ' ' + parameters.sessionParams.join(' ') : '') +
  314. '\r\n';
  315. };
  316. // Parses the crypto key parameters into
  317. // https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam*
  318. SDPUtils.parseCryptoKeyParams = function(keyParams) {
  319. if (keyParams.indexOf('inline:') !== 0) {
  320. return null;
  321. }
  322. const parts = keyParams.substring(7).split('|');
  323. return {
  324. keyMethod: 'inline',
  325. keySalt: parts[0],
  326. lifeTime: parts[1],
  327. mkiValue: parts[2] ? parts[2].split(':')[0] : undefined,
  328. mkiLength: parts[2] ? parts[2].split(':')[1] : undefined,
  329. };
  330. };
  331. SDPUtils.writeCryptoKeyParams = function(keyParams) {
  332. return keyParams.keyMethod + ':'
  333. + keyParams.keySalt +
  334. (keyParams.lifeTime ? '|' + keyParams.lifeTime : '') +
  335. (keyParams.mkiValue && keyParams.mkiLength
  336. ? '|' + keyParams.mkiValue + ':' + keyParams.mkiLength
  337. : '');
  338. };
  339. // Extracts all SDES parameters.
  340. SDPUtils.getCryptoParameters = function(mediaSection, sessionpart) {
  341. const lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
  342. 'a=crypto:');
  343. return lines.map(SDPUtils.parseCryptoLine);
  344. };
  345. // Parses ICE information from SDP media section or sessionpart.
  346. // FIXME: for consistency with other functions this should only
  347. // get the ice-ufrag and ice-pwd lines as input.
  348. SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
  349. const ufrag = SDPUtils.matchPrefix(mediaSection + sessionpart,
  350. 'a=ice-ufrag:')[0];
  351. const pwd = SDPUtils.matchPrefix(mediaSection + sessionpart,
  352. 'a=ice-pwd:')[0];
  353. if (!(ufrag && pwd)) {
  354. return null;
  355. }
  356. return {
  357. usernameFragment: ufrag.substring(12),
  358. password: pwd.substring(10),
  359. };
  360. };
  361. // Serializes ICE parameters to SDP.
  362. SDPUtils.writeIceParameters = function(params) {
  363. let sdp = 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
  364. 'a=ice-pwd:' + params.password + '\r\n';
  365. if (params.iceLite) {
  366. sdp += 'a=ice-lite\r\n';
  367. }
  368. return sdp;
  369. };
  370. // Parses the SDP media section and returns RTCRtpParameters.
  371. SDPUtils.parseRtpParameters = function(mediaSection) {
  372. const description = {
  373. codecs: [],
  374. headerExtensions: [],
  375. fecMechanisms: [],
  376. rtcp: [],
  377. };
  378. const lines = SDPUtils.splitLines(mediaSection);
  379. const mline = lines[0].split(' ');
  380. description.profile = mline[2];
  381. for (let i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
  382. const pt = mline[i];
  383. const rtpmapline = SDPUtils.matchPrefix(
  384. mediaSection, 'a=rtpmap:' + pt + ' ')[0];
  385. if (rtpmapline) {
  386. const codec = SDPUtils.parseRtpMap(rtpmapline);
  387. const fmtps = SDPUtils.matchPrefix(
  388. mediaSection, 'a=fmtp:' + pt + ' ');
  389. // Only the first a=fmtp:<pt> is considered.
  390. codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
  391. codec.rtcpFeedback = SDPUtils.matchPrefix(
  392. mediaSection, 'a=rtcp-fb:' + pt + ' ')
  393. .map(SDPUtils.parseRtcpFb);
  394. description.codecs.push(codec);
  395. // parse FEC mechanisms from rtpmap lines.
  396. switch (codec.name.toUpperCase()) {
  397. case 'RED':
  398. case 'ULPFEC':
  399. description.fecMechanisms.push(codec.name.toUpperCase());
  400. break;
  401. default: // only RED and ULPFEC are recognized as FEC mechanisms.
  402. break;
  403. }
  404. }
  405. }
  406. SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(line => {
  407. description.headerExtensions.push(SDPUtils.parseExtmap(line));
  408. });
  409. const wildcardRtcpFb = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:* ')
  410. .map(SDPUtils.parseRtcpFb);
  411. description.codecs.forEach(codec => {
  412. wildcardRtcpFb.forEach(fb=> {
  413. const duplicate = codec.rtcpFeedback.find(existingFeedback => {
  414. return existingFeedback.type === fb.type &&
  415. existingFeedback.parameter === fb.parameter;
  416. });
  417. if (!duplicate) {
  418. codec.rtcpFeedback.push(fb);
  419. }
  420. });
  421. });
  422. // FIXME: parse rtcp.
  423. return description;
  424. };
  425. // Generates parts of the SDP media section describing the capabilities /
  426. // parameters.
  427. SDPUtils.writeRtpDescription = function(kind, caps) {
  428. let sdp = '';
  429. // Build the mline.
  430. sdp += 'm=' + kind + ' ';
  431. sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
  432. sdp += ' ' + (caps.profile || 'UDP/TLS/RTP/SAVPF') + ' ';
  433. sdp += caps.codecs.map(codec => {
  434. if (codec.preferredPayloadType !== undefined) {
  435. return codec.preferredPayloadType;
  436. }
  437. return codec.payloadType;
  438. }).join(' ') + '\r\n';
  439. sdp += 'c=IN IP4 0.0.0.0\r\n';
  440. sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
  441. // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
  442. caps.codecs.forEach(codec => {
  443. sdp += SDPUtils.writeRtpMap(codec);
  444. sdp += SDPUtils.writeFmtp(codec);
  445. sdp += SDPUtils.writeRtcpFb(codec);
  446. });
  447. let maxptime = 0;
  448. caps.codecs.forEach(codec => {
  449. if (codec.maxptime > maxptime) {
  450. maxptime = codec.maxptime;
  451. }
  452. });
  453. if (maxptime > 0) {
  454. sdp += 'a=maxptime:' + maxptime + '\r\n';
  455. }
  456. if (caps.headerExtensions) {
  457. caps.headerExtensions.forEach(extension => {
  458. sdp += SDPUtils.writeExtmap(extension);
  459. });
  460. }
  461. // FIXME: write fecMechanisms.
  462. return sdp;
  463. };
  464. // Parses the SDP media section and returns an array of
  465. // RTCRtpEncodingParameters.
  466. SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
  467. const encodingParameters = [];
  468. const description = SDPUtils.parseRtpParameters(mediaSection);
  469. const hasRed = description.fecMechanisms.indexOf('RED') !== -1;
  470. const hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
  471. // filter a=ssrc:... cname:, ignore PlanB-msid
  472. const ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
  473. .map(line => SDPUtils.parseSsrcMedia(line))
  474. .filter(parts => parts.attribute === 'cname');
  475. const primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
  476. let secondarySsrc;
  477. const flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
  478. .map(line => {
  479. const parts = line.substring(17).split(' ');
  480. return parts.map(part => parseInt(part, 10));
  481. });
  482. if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
  483. secondarySsrc = flows[0][1];
  484. }
  485. description.codecs.forEach(codec => {
  486. if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
  487. let encParam = {
  488. ssrc: primarySsrc,
  489. codecPayloadType: parseInt(codec.parameters.apt, 10),
  490. };
  491. if (primarySsrc && secondarySsrc) {
  492. encParam.rtx = {ssrc: secondarySsrc};
  493. }
  494. encodingParameters.push(encParam);
  495. if (hasRed) {
  496. encParam = JSON.parse(JSON.stringify(encParam));
  497. encParam.fec = {
  498. ssrc: primarySsrc,
  499. mechanism: hasUlpfec ? 'red+ulpfec' : 'red',
  500. };
  501. encodingParameters.push(encParam);
  502. }
  503. }
  504. });
  505. if (encodingParameters.length === 0 && primarySsrc) {
  506. encodingParameters.push({
  507. ssrc: primarySsrc,
  508. });
  509. }
  510. // we support both b=AS and b=TIAS but interpret AS as TIAS.
  511. let bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
  512. if (bandwidth.length) {
  513. if (bandwidth[0].indexOf('b=TIAS:') === 0) {
  514. bandwidth = parseInt(bandwidth[0].substring(7), 10);
  515. } else if (bandwidth[0].indexOf('b=AS:') === 0) {
  516. // use formula from JSEP to convert b=AS to TIAS value.
  517. bandwidth = parseInt(bandwidth[0].substring(5), 10) * 1000 * 0.95
  518. - (50 * 40 * 8);
  519. } else {
  520. bandwidth = undefined;
  521. }
  522. encodingParameters.forEach(params => {
  523. params.maxBitrate = bandwidth;
  524. });
  525. }
  526. return encodingParameters;
  527. };
  528. // parses http://draft.ortc.org/#rtcrtcpparameters*
  529. SDPUtils.parseRtcpParameters = function(mediaSection) {
  530. const rtcpParameters = {};
  531. // Gets the first SSRC. Note that with RTX there might be multiple
  532. // SSRCs.
  533. const remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
  534. .map(line => SDPUtils.parseSsrcMedia(line))
  535. .filter(obj => obj.attribute === 'cname')[0];
  536. if (remoteSsrc) {
  537. rtcpParameters.cname = remoteSsrc.value;
  538. rtcpParameters.ssrc = remoteSsrc.ssrc;
  539. }
  540. // Edge uses the compound attribute instead of reducedSize
  541. // compound is !reducedSize
  542. const rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
  543. rtcpParameters.reducedSize = rsize.length > 0;
  544. rtcpParameters.compound = rsize.length === 0;
  545. // parses the rtcp-mux attrіbute.
  546. // Note that Edge does not support unmuxed RTCP.
  547. const mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
  548. rtcpParameters.mux = mux.length > 0;
  549. return rtcpParameters;
  550. };
  551. SDPUtils.writeRtcpParameters = function(rtcpParameters) {
  552. let sdp = '';
  553. if (rtcpParameters.reducedSize) {
  554. sdp += 'a=rtcp-rsize\r\n';
  555. }
  556. if (rtcpParameters.mux) {
  557. sdp += 'a=rtcp-mux\r\n';
  558. }
  559. if (rtcpParameters.ssrc !== undefined && rtcpParameters.cname) {
  560. sdp += 'a=ssrc:' + rtcpParameters.ssrc +
  561. ' cname:' + rtcpParameters.cname + '\r\n';
  562. }
  563. return sdp;
  564. };
  565. // parses either a=msid: or a=ssrc:... msid lines and returns
  566. // the id of the MediaStream and MediaStreamTrack.
  567. SDPUtils.parseMsid = function(mediaSection) {
  568. let parts;
  569. const spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
  570. if (spec.length === 1) {
  571. parts = spec[0].substring(7).split(' ');
  572. return {stream: parts[0], track: parts[1]};
  573. }
  574. const planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
  575. .map(line => SDPUtils.parseSsrcMedia(line))
  576. .filter(msidParts => msidParts.attribute === 'msid');
  577. if (planB.length > 0) {
  578. parts = planB[0].value.split(' ');
  579. return {stream: parts[0], track: parts[1]};
  580. }
  581. };
  582. // SCTP
  583. // parses draft-ietf-mmusic-sctp-sdp-26 first and falls back
  584. // to draft-ietf-mmusic-sctp-sdp-05
  585. SDPUtils.parseSctpDescription = function(mediaSection) {
  586. const mline = SDPUtils.parseMLine(mediaSection);
  587. const maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:');
  588. let maxMessageSize;
  589. if (maxSizeLine.length > 0) {
  590. maxMessageSize = parseInt(maxSizeLine[0].substring(19), 10);
  591. }
  592. if (isNaN(maxMessageSize)) {
  593. maxMessageSize = 65536;
  594. }
  595. const sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:');
  596. if (sctpPort.length > 0) {
  597. return {
  598. port: parseInt(sctpPort[0].substring(12), 10),
  599. protocol: mline.fmt,
  600. maxMessageSize,
  601. };
  602. }
  603. const sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:');
  604. if (sctpMapLines.length > 0) {
  605. const parts = sctpMapLines[0]
  606. .substring(10)
  607. .split(' ');
  608. return {
  609. port: parseInt(parts[0], 10),
  610. protocol: parts[1],
  611. maxMessageSize,
  612. };
  613. }
  614. };
  615. // SCTP
  616. // outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers
  617. // support by now receiving in this format, unless we originally parsed
  618. // as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line
  619. // protocol of DTLS/SCTP -- without UDP/ or TCP/)
  620. SDPUtils.writeSctpDescription = function(media, sctp) {
  621. let output = [];
  622. if (media.protocol !== 'DTLS/SCTP') {
  623. output = [
  624. 'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n',
  625. 'c=IN IP4 0.0.0.0\r\n',
  626. 'a=sctp-port:' + sctp.port + '\r\n',
  627. ];
  628. } else {
  629. output = [
  630. 'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n',
  631. 'c=IN IP4 0.0.0.0\r\n',
  632. 'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n',
  633. ];
  634. }
  635. if (sctp.maxMessageSize !== undefined) {
  636. output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n');
  637. }
  638. return output.join('');
  639. };
  640. // Generate a session ID for SDP.
  641. // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
  642. // recommends using a cryptographically random +ve 64-bit value
  643. // but right now this should be acceptable and within the right range
  644. SDPUtils.generateSessionId = function() {
  645. return Math.random().toString().substr(2, 22);
  646. };
  647. // Write boiler plate for start of SDP
  648. // sessId argument is optional - if not supplied it will
  649. // be generated randomly
  650. // sessVersion is optional and defaults to 2
  651. // sessUser is optional and defaults to 'thisisadapterortc'
  652. SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) {
  653. let sessionId;
  654. const version = sessVer !== undefined ? sessVer : 2;
  655. if (sessId) {
  656. sessionId = sessId;
  657. } else {
  658. sessionId = SDPUtils.generateSessionId();
  659. }
  660. const user = sessUser || 'thisisadapterortc';
  661. // FIXME: sess-id should be an NTP timestamp.
  662. return 'v=0\r\n' +
  663. 'o=' + user + ' ' + sessionId + ' ' + version +
  664. ' IN IP4 127.0.0.1\r\n' +
  665. 's=-\r\n' +
  666. 't=0 0\r\n';
  667. };
  668. // Gets the direction from the mediaSection or the sessionpart.
  669. SDPUtils.getDirection = function(mediaSection, sessionpart) {
  670. // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
  671. const lines = SDPUtils.splitLines(mediaSection);
  672. for (let i = 0; i < lines.length; i++) {
  673. switch (lines[i]) {
  674. case 'a=sendrecv':
  675. case 'a=sendonly':
  676. case 'a=recvonly':
  677. case 'a=inactive':
  678. return lines[i].substring(2);
  679. default:
  680. // FIXME: What should happen here?
  681. }
  682. }
  683. if (sessionpart) {
  684. return SDPUtils.getDirection(sessionpart);
  685. }
  686. return 'sendrecv';
  687. };
  688. SDPUtils.getKind = function(mediaSection) {
  689. const lines = SDPUtils.splitLines(mediaSection);
  690. const mline = lines[0].split(' ');
  691. return mline[0].substring(2);
  692. };
  693. SDPUtils.isRejected = function(mediaSection) {
  694. return mediaSection.split(' ', 2)[1] === '0';
  695. };
  696. SDPUtils.parseMLine = function(mediaSection) {
  697. const lines = SDPUtils.splitLines(mediaSection);
  698. const parts = lines[0].substring(2).split(' ');
  699. return {
  700. kind: parts[0],
  701. port: parseInt(parts[1], 10),
  702. protocol: parts[2],
  703. fmt: parts.slice(3).join(' '),
  704. };
  705. };
  706. SDPUtils.parseOLine = function(mediaSection) {
  707. const line = SDPUtils.matchPrefix(mediaSection, 'o=')[0];
  708. const parts = line.substring(2).split(' ');
  709. return {
  710. username: parts[0],
  711. sessionId: parts[1],
  712. sessionVersion: parseInt(parts[2], 10),
  713. netType: parts[3],
  714. addressType: parts[4],
  715. address: parts[5],
  716. };
  717. };
  718. // a very naive interpretation of a valid SDP.
  719. SDPUtils.isValidSDP = function(blob) {
  720. if (typeof blob !== 'string' || blob.length === 0) {
  721. return false;
  722. }
  723. const lines = SDPUtils.splitLines(blob);
  724. for (let i = 0; i < lines.length; i++) {
  725. if (lines[i].length < 2 || lines[i].charAt(1) !== '=') {
  726. return false;
  727. }
  728. // TODO: check the modifier a bit more.
  729. }
  730. return true;
  731. };
  732. // Expose public methods.
  733. if (typeof module === 'object') {
  734. module.exports = SDPUtils;
  735. }