You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SDPUtil.js 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. import { getLogger } from 'jitsi-meet-logger';
  2. const logger = getLogger(__filename);
  3. import RandomUtil from '../util/RandomUtil';
  4. import browser from '../browser';
  5. const SDPUtil = {
  6. filterSpecialChars(text) {
  7. // XXX Neither one of the falsy values (e.g. null, undefined, false,
  8. // "", etc.) "contain" special chars.
  9. // eslint-disable-next-line no-useless-escape
  10. return text ? text.replace(/[\\\/\{,\}\+]/g, '') : text;
  11. },
  12. iceparams(mediadesc, sessiondesc) {
  13. let data = null;
  14. let pwd, ufrag;
  15. if ((ufrag = SDPUtil.findLine(mediadesc, 'a=ice-ufrag:', sessiondesc))
  16. && (pwd
  17. = SDPUtil.findLine(
  18. mediadesc,
  19. 'a=ice-pwd:',
  20. sessiondesc))) {
  21. data = {
  22. ufrag: SDPUtil.parseICEUfrag(ufrag),
  23. pwd: SDPUtil.parseICEPwd(pwd)
  24. };
  25. }
  26. return data;
  27. },
  28. parseICEUfrag(line) {
  29. return line.substring(12);
  30. },
  31. buildICEUfrag(frag) {
  32. return `a=ice-ufrag:${frag}`;
  33. },
  34. parseICEPwd(line) {
  35. return line.substring(10);
  36. },
  37. buildICEPwd(pwd) {
  38. return `a=ice-pwd:${pwd}`;
  39. },
  40. parseMID(line) {
  41. return line.substring(6);
  42. },
  43. parseMLine(line) {
  44. const data = {};
  45. const parts = line.substring(2).split(' ');
  46. data.media = parts.shift();
  47. data.port = parts.shift();
  48. data.proto = parts.shift();
  49. if (parts[parts.length - 1] === '') { // trailing whitespace
  50. parts.pop();
  51. }
  52. data.fmt = parts;
  53. return data;
  54. },
  55. buildMLine(mline) {
  56. return (
  57. `m=${mline.media} ${mline.port} ${mline.proto} ${
  58. mline.fmt.join(' ')}`);
  59. },
  60. parseRTPMap(line) {
  61. const data = {};
  62. let parts = line.substring(9).split(' ');
  63. data.id = parts.shift();
  64. parts = parts[0].split('/');
  65. data.name = parts.shift();
  66. data.clockrate = parts.shift();
  67. data.channels = parts.length ? parts.shift() : '1';
  68. return data;
  69. },
  70. /**
  71. * Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
  72. * @param line eg. "a=sctpmap:5000 webrtc-datachannel"
  73. * @returns [SCTP port number, protocol, streams]
  74. */
  75. parseSCTPMap(line) {
  76. const parts = line.substring(10).split(' ');
  77. const sctpPort = parts[0];
  78. const protocol = parts[1];
  79. // Stream count is optional
  80. const streamCount = parts.length > 2 ? parts[2] : null;
  81. return [ sctpPort, protocol, streamCount ];// SCTP port
  82. },
  83. buildRTPMap(el) {
  84. let line
  85. = `a=rtpmap:${el.getAttribute('id')} ${el.getAttribute('name')}/${
  86. el.getAttribute('clockrate')}`;
  87. if (el.getAttribute('channels')
  88. && el.getAttribute('channels') !== '1') {
  89. line += `/${el.getAttribute('channels')}`;
  90. }
  91. return line;
  92. },
  93. parseCrypto(line) {
  94. const data = {};
  95. const parts = line.substring(9).split(' ');
  96. data.tag = parts.shift();
  97. data['crypto-suite'] = parts.shift();
  98. data['key-params'] = parts.shift();
  99. if (parts.length) {
  100. data['session-params'] = parts.join(' ');
  101. }
  102. return data;
  103. },
  104. parseFingerprint(line) { // RFC 4572
  105. const data = {};
  106. const parts = line.substring(14).split(' ');
  107. data.hash = parts.shift();
  108. data.fingerprint = parts.shift();
  109. // TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
  110. return data;
  111. },
  112. parseFmtp(line) {
  113. const data = [];
  114. let parts = line.split(' ');
  115. parts.shift();
  116. parts = parts.join(' ').split(';');
  117. for (let i = 0; i < parts.length; i++) {
  118. let key = parts[i].split('=')[0];
  119. while (key.length && key[0] === ' ') {
  120. key = key.substring(1);
  121. }
  122. const value = parts[i].split('=')[1];
  123. if (key && value) {
  124. data.push({ name: key,
  125. value });
  126. } else if (key) {
  127. // rfc 4733 (DTMF) style stuff
  128. data.push({ name: '',
  129. value: key });
  130. }
  131. }
  132. return data;
  133. },
  134. parseICECandidate(line) {
  135. const candidate = {};
  136. const elems = line.split(' ');
  137. candidate.foundation = elems[0].substring(12);
  138. candidate.component = elems[1];
  139. candidate.protocol = elems[2].toLowerCase();
  140. candidate.priority = elems[3];
  141. candidate.ip = elems[4];
  142. candidate.port = elems[5];
  143. // elems[6] => "typ"
  144. candidate.type = elems[7];
  145. candidate.generation = 0; // default value, may be overwritten below
  146. for (let i = 8; i < elems.length; i += 2) {
  147. switch (elems[i]) {
  148. case 'raddr':
  149. candidate['rel-addr'] = elems[i + 1];
  150. break;
  151. case 'rport':
  152. candidate['rel-port'] = elems[i + 1];
  153. break;
  154. case 'generation':
  155. candidate.generation = elems[i + 1];
  156. break;
  157. case 'tcptype':
  158. candidate.tcptype = elems[i + 1];
  159. break;
  160. default: // TODO
  161. logger.log(
  162. `parseICECandidate not translating "${
  163. elems[i]}" = "${elems[i + 1]}"`);
  164. }
  165. }
  166. candidate.network = '1';
  167. // not applicable to SDP -- FIXME: should be unique, not just random
  168. // eslint-disable-next-line newline-per-chained-call
  169. candidate.id = Math.random().toString(36).substr(2, 10);
  170. return candidate;
  171. },
  172. buildICECandidate(cand) {
  173. let line = [
  174. `a=candidate:${cand.foundation}`,
  175. cand.component,
  176. cand.protocol,
  177. cand.priority,
  178. cand.ip,
  179. cand.port,
  180. 'typ',
  181. cand.type
  182. ].join(' ');
  183. line += ' ';
  184. switch (cand.type) {
  185. case 'srflx':
  186. case 'prflx':
  187. case 'relay':
  188. if (cand.hasOwnAttribute('rel-addr')
  189. && cand.hasOwnAttribute('rel-port')) {
  190. line += 'raddr';
  191. line += ' ';
  192. line += cand['rel-addr'];
  193. line += ' ';
  194. line += 'rport';
  195. line += ' ';
  196. line += cand['rel-port'];
  197. line += ' ';
  198. }
  199. break;
  200. }
  201. if (cand.hasOwnAttribute('tcptype')) {
  202. line += 'tcptype';
  203. line += ' ';
  204. line += cand.tcptype;
  205. line += ' ';
  206. }
  207. line += 'generation';
  208. line += ' ';
  209. line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
  210. return line;
  211. },
  212. parseSSRC(desc) {
  213. // proprietary mapping of a=ssrc lines
  214. // TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher
  215. // on google docs and parse according to that
  216. const data = new Map();
  217. const lines = desc.split('\r\n');
  218. for (let i = 0; i < lines.length; i++) {
  219. if (lines[i].substring(0, 7) === 'a=ssrc:') {
  220. // FIXME: Use regex to smartly find the ssrc.
  221. const ssrc = lines[i].split('a=ssrc:')[1].split(' ')[0];
  222. if (!data.get(ssrc)) {
  223. data.set(ssrc, []);
  224. }
  225. data.get(ssrc).push(lines[i]);
  226. }
  227. }
  228. return data;
  229. },
  230. parseRTCPFB(line) {
  231. const parts = line.substr(10).split(' ');
  232. const data = {};
  233. data.pt = parts.shift();
  234. data.type = parts.shift();
  235. data.params = parts;
  236. return data;
  237. },
  238. parseExtmap(line) {
  239. const parts = line.substr(9).split(' ');
  240. const data = {};
  241. data.value = parts.shift();
  242. if (data.value.indexOf('/') === -1) {
  243. data.direction = 'both';
  244. } else {
  245. data.direction = data.value.substr(data.value.indexOf('/') + 1);
  246. data.value = data.value.substr(0, data.value.indexOf('/'));
  247. }
  248. data.uri = parts.shift();
  249. data.params = parts;
  250. return data;
  251. },
  252. findLine(haystack, needle, sessionpart) {
  253. let lines = haystack.split('\r\n');
  254. for (let i = 0; i < lines.length; i++) {
  255. if (lines[i].substring(0, needle.length) === needle) {
  256. return lines[i];
  257. }
  258. }
  259. if (!sessionpart) {
  260. return false;
  261. }
  262. // search session part
  263. lines = sessionpart.split('\r\n');
  264. for (let j = 0; j < lines.length; j++) {
  265. if (lines[j].substring(0, needle.length) === needle) {
  266. return lines[j];
  267. }
  268. }
  269. return false;
  270. },
  271. findLines(haystack, needle, sessionpart) {
  272. let lines = haystack.split('\r\n');
  273. const needles = [];
  274. for (let i = 0; i < lines.length; i++) {
  275. if (lines[i].substring(0, needle.length) === needle) {
  276. needles.push(lines[i]);
  277. }
  278. }
  279. if (needles.length || !sessionpart) {
  280. return needles;
  281. }
  282. // search session part
  283. lines = sessionpart.split('\r\n');
  284. for (let j = 0; j < lines.length; j++) {
  285. if (lines[j].substring(0, needle.length) === needle) {
  286. needles.push(lines[j]);
  287. }
  288. }
  289. return needles;
  290. },
  291. candidateToJingle(line) {
  292. // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host
  293. // generation 0
  294. // <candidate component=... foundation=... generation=... id=...
  295. // ip=... network=... port=... priority=... protocol=... type=.../>
  296. if (line.indexOf('candidate:') === 0) {
  297. // eslint-disable-next-line no-param-reassign
  298. line = `a=${line}`;
  299. } else if (line.substring(0, 12) !== 'a=candidate:') {
  300. logger.log(
  301. 'parseCandidate called with a line that is not a candidate'
  302. + ' line');
  303. logger.log(line);
  304. return null;
  305. }
  306. if (line.substring(line.length - 2) === '\r\n') { // chomp it
  307. // eslint-disable-next-line no-param-reassign
  308. line = line.substring(0, line.length - 2);
  309. }
  310. const candidate = {};
  311. const elems = line.split(' ');
  312. if (elems[6] !== 'typ') {
  313. logger.log('did not find typ in the right place');
  314. logger.log(line);
  315. return null;
  316. }
  317. candidate.foundation = elems[0].substring(12);
  318. candidate.component = elems[1];
  319. candidate.protocol = elems[2].toLowerCase();
  320. candidate.priority = elems[3];
  321. candidate.ip = elems[4];
  322. candidate.port = elems[5];
  323. // elems[6] => "typ"
  324. candidate.type = elems[7];
  325. candidate.generation = '0'; // default, may be overwritten below
  326. for (let i = 8; i < elems.length; i += 2) {
  327. switch (elems[i]) {
  328. case 'raddr':
  329. candidate['rel-addr'] = elems[i + 1];
  330. break;
  331. case 'rport':
  332. candidate['rel-port'] = elems[i + 1];
  333. break;
  334. case 'generation':
  335. candidate.generation = elems[i + 1];
  336. break;
  337. case 'tcptype':
  338. candidate.tcptype = elems[i + 1];
  339. break;
  340. default: // TODO
  341. logger.log(`not translating "${elems[i]}" = "${elems[i + 1]}"`);
  342. }
  343. }
  344. candidate.network = '1';
  345. // not applicable to SDP -- FIXME: should be unique, not just random
  346. // eslint-disable-next-line newline-per-chained-call
  347. candidate.id = Math.random().toString(36).substr(2, 10);
  348. return candidate;
  349. },
  350. candidateFromJingle(cand) {
  351. let line = 'a=candidate:';
  352. line += cand.getAttribute('foundation');
  353. line += ' ';
  354. line += cand.getAttribute('component');
  355. line += ' ';
  356. let protocol = cand.getAttribute('protocol');
  357. // use tcp candidates for FF
  358. if (browser.isFirefox() && protocol.toLowerCase() === 'ssltcp') {
  359. protocol = 'tcp';
  360. }
  361. line += protocol; // .toUpperCase(); // chrome M23 doesn't like this
  362. line += ' ';
  363. line += cand.getAttribute('priority');
  364. line += ' ';
  365. line += cand.getAttribute('ip');
  366. line += ' ';
  367. line += cand.getAttribute('port');
  368. line += ' ';
  369. line += 'typ';
  370. line += ` ${cand.getAttribute('type')}`;
  371. line += ' ';
  372. switch (cand.getAttribute('type')) {
  373. case 'srflx':
  374. case 'prflx':
  375. case 'relay':
  376. if (cand.getAttribute('rel-addr')
  377. && cand.getAttribute('rel-port')) {
  378. line += 'raddr';
  379. line += ' ';
  380. line += cand.getAttribute('rel-addr');
  381. line += ' ';
  382. line += 'rport';
  383. line += ' ';
  384. line += cand.getAttribute('rel-port');
  385. line += ' ';
  386. }
  387. break;
  388. }
  389. if (protocol.toLowerCase() === 'tcp') {
  390. line += 'tcptype';
  391. line += ' ';
  392. line += cand.getAttribute('tcptype');
  393. line += ' ';
  394. }
  395. line += 'generation';
  396. line += ' ';
  397. line += cand.getAttribute('generation') || '0';
  398. return `${line}\r\n`;
  399. },
  400. /**
  401. * Parse the 'most' primary video ssrc from the given m line
  402. * @param {object} mLine object as parsed from transform.parse
  403. * @return {number} the primary video ssrc from the given m line
  404. */
  405. parsePrimaryVideoSsrc(videoMLine) {
  406. const numSsrcs = videoMLine.ssrcs
  407. .map(ssrcInfo => ssrcInfo.id)
  408. .filter((ssrc, index, array) => array.indexOf(ssrc) === index)
  409. .length;
  410. const numGroups
  411. = (videoMLine.ssrcGroups && videoMLine.ssrcGroups.length) || 0;
  412. if (numSsrcs > 1 && numGroups === 0) {
  413. // Ambiguous, can't figure out the primary
  414. return;
  415. }
  416. let primarySsrc = null;
  417. if (numSsrcs === 1) {
  418. primarySsrc = videoMLine.ssrcs[0].id;
  419. } else if (numSsrcs === 2) {
  420. // Can figure it out if there's an FID group
  421. const fidGroup
  422. = videoMLine.ssrcGroups.find(
  423. group => group.semantics === 'FID');
  424. if (fidGroup) {
  425. primarySsrc = fidGroup.ssrcs.split(' ')[0];
  426. }
  427. } else if (numSsrcs >= 3) {
  428. // Can figure it out if there's a sim group
  429. const simGroup
  430. = videoMLine.ssrcGroups.find(
  431. group => group.semantics === 'SIM');
  432. if (simGroup) {
  433. primarySsrc = simGroup.ssrcs.split(' ')[0];
  434. }
  435. }
  436. return primarySsrc;
  437. },
  438. /**
  439. * Generate an ssrc
  440. * @returns {number} an ssrc
  441. */
  442. generateSsrc() {
  443. return RandomUtil.randomInt(1, 0xffffffff);
  444. },
  445. /**
  446. * Get an attribute for the given ssrc with the given attributeName
  447. * from the given mline
  448. * @param {object} mLine an mLine object as parsed from transform.parse
  449. * @param {number} ssrc the ssrc for which an attribute is desired
  450. * @param {string} attributeName the name of the desired attribute
  451. * @returns {string} the value corresponding to the given ssrc
  452. * and attributeName
  453. */
  454. getSsrcAttribute(mLine, ssrc, attributeName) {
  455. for (let i = 0; i < mLine.ssrcs.length; ++i) {
  456. const ssrcLine = mLine.ssrcs[i];
  457. if (ssrcLine.id === ssrc
  458. && ssrcLine.attribute === attributeName) {
  459. return ssrcLine.value;
  460. }
  461. }
  462. },
  463. /**
  464. * Parses the ssrcs from the group sdp line and
  465. * returns them as a list of numbers
  466. * @param {object} the ssrcGroup object as parsed from
  467. * sdp-transform
  468. * @returns {list<number>} a list of the ssrcs in the group
  469. * parsed as numbers
  470. */
  471. parseGroupSsrcs(ssrcGroup) {
  472. return ssrcGroup
  473. .ssrcs
  474. .split(' ')
  475. .map(ssrcStr => parseInt(ssrcStr, 10));
  476. },
  477. /**
  478. * Get the mline of the given type from the given sdp
  479. * @param {object} sdp sdp as parsed from transform.parse
  480. * @param {string} type the type of the desired mline (e.g. "video")
  481. * @returns {object} a media object
  482. */
  483. getMedia(sdp, type) {
  484. return sdp.media.find(m => m.type === type);
  485. },
  486. /**
  487. * Extracts the ICE username fragment from an SDP string.
  488. * @param {string} sdp the SDP in raw text format
  489. */
  490. getUfrag(sdp) {
  491. const ufragLines
  492. = sdp.split('\n').filter(line => line.startsWith('a=ice-ufrag:'));
  493. if (ufragLines.length > 0) {
  494. return ufragLines[0].substr('a=ice-ufrag:'.length);
  495. }
  496. },
  497. /**
  498. * Sets the given codecName as the preferred codec by
  499. * moving it to the beginning of the payload types
  500. * list (modifies the given mline in place). If there
  501. * are multiple options within the same codec (multiple h264
  502. * profiles, for instance), this will prefer the first one
  503. * that is found.
  504. * @param {object} videoMLine the video mline object from
  505. * an sdp as parsed by transform.parse
  506. * @param {string} codecName the name of the preferred codec
  507. */
  508. preferVideoCodec(videoMLine, codecName) {
  509. let payloadType = null;
  510. if (!videoMLine || !codecName) {
  511. return;
  512. }
  513. for (let i = 0; i < videoMLine.rtp.length; ++i) {
  514. const rtp = videoMLine.rtp[i];
  515. if (rtp.codec
  516. && rtp.codec.toLowerCase() === codecName.toLowerCase()) {
  517. payloadType = rtp.payload;
  518. break;
  519. }
  520. }
  521. if (payloadType) {
  522. // Call toString() on payloads to get around an issue within
  523. // SDPTransform that sets payloads as a number, instead of a string,
  524. // when there is only one payload.
  525. const payloadTypes
  526. = videoMLine.payloads
  527. .toString()
  528. .split(' ')
  529. .map(p => parseInt(p, 10));
  530. const payloadIndex = payloadTypes.indexOf(payloadType);
  531. payloadTypes.splice(payloadIndex, 1);
  532. payloadTypes.unshift(payloadType);
  533. videoMLine.payloads = payloadTypes.join(' ');
  534. }
  535. },
  536. /**
  537. * Strips the given codec from the given mline. All related RTX payload
  538. * types are also stripped. If the resulting mline would have no codecs,
  539. * it's disabled.
  540. *
  541. * @param {object} videoMLine the video mline object from an sdp as parsed
  542. * by transform.parse.
  543. * @param {string} codecName the name of the codec which will be stripped.
  544. */
  545. stripVideoCodec(videoMLine, codecName) {
  546. if (!videoMLine || !codecName) {
  547. return;
  548. }
  549. const removePts = [];
  550. for (const rtp of videoMLine.rtp) {
  551. if (rtp.codec
  552. && rtp.codec.toLowerCase() === codecName.toLowerCase()) {
  553. removePts.push(rtp.payload);
  554. }
  555. }
  556. if (removePts.length > 0) {
  557. // We also need to remove the payload types that are related to RTX
  558. // for the codecs we want to disable.
  559. const rtxApts = removePts.map(item => `apt=${item}`);
  560. const rtxPts = videoMLine.fmtp.filter(
  561. item => rtxApts.indexOf(item.config) !== -1);
  562. removePts.push(...rtxPts.map(item => item.payload));
  563. // Call toString() on payloads to get around an issue within
  564. // SDPTransform that sets payloads as a number, instead of a string,
  565. // when there is only one payload.
  566. const allPts = videoMLine.payloads
  567. .toString()
  568. .split(' ')
  569. .map(Number);
  570. const keepPts = allPts.filter(pt => removePts.indexOf(pt) === -1);
  571. if (keepPts.length === 0) {
  572. // There are no other video codecs, disable the stream.
  573. videoMLine.port = 0;
  574. videoMLine.direction = 'inactive';
  575. videoMLine.payloads = '*';
  576. } else {
  577. videoMLine.payloads = keepPts.join(' ');
  578. }
  579. videoMLine.rtp = videoMLine.rtp.filter(
  580. item => keepPts.indexOf(item.payload) !== -1);
  581. videoMLine.fmtp = videoMLine.fmtp.filter(
  582. item => keepPts.indexOf(item.payload) !== -1);
  583. if (videoMLine.rtcpFb) {
  584. videoMLine.rtcpFb = videoMLine.rtcpFb.filter(
  585. item => keepPts.indexOf(item.payload) !== -1);
  586. }
  587. }
  588. }
  589. };
  590. export default SDPUtil;