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

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