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 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. import { getLogger } from 'jitsi-meet-logger';
  2. const logger = getLogger(__filename);
  3. import RandomUtil from '../util/RandomUtil';
  4. const RTCBrowserType = require('../RTC/RTCBrowserType');
  5. const SDPUtil = {
  6. filter_special_chars(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.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc))
  16. && (pwd
  17. = SDPUtil.find_line(
  18. mediadesc,
  19. 'a=ice-pwd:',
  20. sessiondesc))) {
  21. data = {
  22. ufrag: SDPUtil.parse_iceufrag(ufrag),
  23. pwd: SDPUtil.parse_icepwd(pwd)
  24. };
  25. }
  26. return data;
  27. },
  28. parse_iceufrag(line) {
  29. return line.substring(12);
  30. },
  31. build_iceufrag(frag) {
  32. return `a=ice-ufrag:${frag}`;
  33. },
  34. parse_icepwd(line) {
  35. return line.substring(10);
  36. },
  37. build_icepwd(pwd) {
  38. return `a=ice-pwd:${pwd}`;
  39. },
  40. parse_mid(line) {
  41. return line.substring(6);
  42. },
  43. parse_mline(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. build_mline(mline) {
  56. return (
  57. `m=${mline.media} ${mline.port} ${mline.proto} ${
  58. mline.fmt.join(' ')}`);
  59. },
  60. parse_rtpmap(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. parse_sctpmap(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. build_rtpmap(el) {
  84. let line
  85. = `a=rtpmap:${el.getAttribute('id')} ${el.getAttribute('name')}/${
  86. el.getAttribute('clockrate')}`;
  87. if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
  88. line += `/${el.getAttribute('channels')}`;
  89. }
  90. return line;
  91. },
  92. parse_crypto(line) {
  93. const data = {};
  94. const parts = line.substring(9).split(' ');
  95. data.tag = parts.shift();
  96. data['crypto-suite'] = parts.shift();
  97. data['key-params'] = parts.shift();
  98. if (parts.length) {
  99. data['session-params'] = parts.join(' ');
  100. }
  101. return data;
  102. },
  103. parse_fingerprint(line) { // RFC 4572
  104. const data = {};
  105. const parts = line.substring(14).split(' ');
  106. data.hash = parts.shift();
  107. data.fingerprint = parts.shift();
  108. // TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
  109. return data;
  110. },
  111. parse_fmtp(line) {
  112. const data = [];
  113. let parts = line.split(' ');
  114. parts.shift();
  115. parts = parts.join(' ').split(';');
  116. for (let i = 0; i < parts.length; i++) {
  117. let key = parts[i].split('=')[0];
  118. while (key.length && key[0] == ' ') {
  119. key = key.substring(1);
  120. }
  121. const value = parts[i].split('=')[1];
  122. if (key && value) {
  123. data.push({ name: key,
  124. value });
  125. } else if (key) {
  126. // rfc 4733 (DTMF) style stuff
  127. data.push({ name: '',
  128. value: key });
  129. }
  130. }
  131. return data;
  132. },
  133. parse_icecandidate(line) {
  134. const candidate = {};
  135. const elems = line.split(' ');
  136. candidate.foundation = elems[0].substring(12);
  137. candidate.component = elems[1];
  138. candidate.protocol = elems[2].toLowerCase();
  139. candidate.priority = elems[3];
  140. candidate.ip = elems[4];
  141. candidate.port = elems[5];
  142. // elems[6] => "typ"
  143. candidate.type = elems[7];
  144. candidate.generation = 0; // default value, may be overwritten below
  145. for (let i = 8; i < elems.length; i += 2) {
  146. switch (elems[i]) {
  147. case 'raddr':
  148. candidate['rel-addr'] = elems[i + 1];
  149. break;
  150. case 'rport':
  151. candidate['rel-port'] = elems[i + 1];
  152. break;
  153. case 'generation':
  154. candidate.generation = elems[i + 1];
  155. break;
  156. case 'tcptype':
  157. candidate.tcptype = elems[i + 1];
  158. break;
  159. default: // TODO
  160. logger.log(
  161. `parse_icecandidate not translating "${
  162. elems[i]}" = "${elems[i + 1]}"`);
  163. }
  164. }
  165. candidate.network = '1';
  166. // not applicable to SDP -- FIXME: should be unique, not just random
  167. // eslint-disable-next-line newline-per-chained-call
  168. candidate.id = Math.random().toString(36).substr(2, 10);
  169. return candidate;
  170. },
  171. build_icecandidate(cand) {
  172. let line = [
  173. `a=candidate:${cand.foundation}`,
  174. cand.component,
  175. cand.protocol,
  176. cand.priority,
  177. cand.ip,
  178. cand.port,
  179. 'typ',
  180. cand.type
  181. ].join(' ');
  182. line += ' ';
  183. switch (cand.type) {
  184. case 'srflx':
  185. case 'prflx':
  186. case 'relay':
  187. if (cand.hasOwnAttribute('rel-addr')
  188. && cand.hasOwnAttribute('rel-port')) {
  189. line += 'raddr';
  190. line += ' ';
  191. line += cand['rel-addr'];
  192. line += ' ';
  193. line += 'rport';
  194. line += ' ';
  195. line += cand['rel-port'];
  196. line += ' ';
  197. }
  198. break;
  199. }
  200. if (cand.hasOwnAttribute('tcptype')) {
  201. line += 'tcptype';
  202. line += ' ';
  203. line += cand.tcptype;
  204. line += ' ';
  205. }
  206. line += 'generation';
  207. line += ' ';
  208. line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
  209. return line;
  210. },
  211. parse_ssrc(desc) {
  212. // proprietary mapping of a=ssrc lines
  213. // TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher
  214. // on google docs and parse according to that
  215. const data = {};
  216. const lines = desc.split('\r\n');
  217. for (let i = 0; i < lines.length; i++) {
  218. if (lines[i].substring(0, 7) == 'a=ssrc:') {
  219. const idx = lines[i].indexOf(' ');
  220. data[lines[i].substr(idx + 1).split(':', 2)[0]]
  221. = lines[i].substr(idx + 1).split(':', 2)[1];
  222. }
  223. }
  224. return data;
  225. },
  226. parse_rtcpfb(line) {
  227. const parts = line.substr(10).split(' ');
  228. const data = {};
  229. data.pt = parts.shift();
  230. data.type = parts.shift();
  231. data.params = parts;
  232. return data;
  233. },
  234. parse_extmap(line) {
  235. const parts = line.substr(9).split(' ');
  236. const data = {};
  237. data.value = parts.shift();
  238. if (data.value.indexOf('/') === -1) {
  239. data.direction = 'both';
  240. } else {
  241. data.direction = data.value.substr(data.value.indexOf('/') + 1);
  242. data.value = data.value.substr(0, data.value.indexOf('/'));
  243. }
  244. data.uri = parts.shift();
  245. data.params = parts;
  246. return data;
  247. },
  248. find_line(haystack, needle, sessionpart) {
  249. let lines = haystack.split('\r\n');
  250. for (let i = 0; i < lines.length; i++) {
  251. if (lines[i].substring(0, needle.length) == needle) {
  252. return lines[i];
  253. }
  254. }
  255. if (!sessionpart) {
  256. return false;
  257. }
  258. // search session part
  259. lines = sessionpart.split('\r\n');
  260. for (let j = 0; j < lines.length; j++) {
  261. if (lines[j].substring(0, needle.length) == needle) {
  262. return lines[j];
  263. }
  264. }
  265. return false;
  266. },
  267. find_lines(haystack, needle, sessionpart) {
  268. let lines = haystack.split('\r\n');
  269. const needles = [];
  270. for (let i = 0; i < lines.length; i++) {
  271. if (lines[i].substring(0, needle.length) == needle) {
  272. needles.push(lines[i]);
  273. }
  274. }
  275. if (needles.length || !sessionpart) {
  276. return needles;
  277. }
  278. // search session part
  279. lines = sessionpart.split('\r\n');
  280. for (let j = 0; j < lines.length; j++) {
  281. if (lines[j].substring(0, needle.length) == needle) {
  282. needles.push(lines[j]);
  283. }
  284. }
  285. return needles;
  286. },
  287. candidateToJingle(line) {
  288. // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host
  289. // generation 0
  290. // <candidate component=... foundation=... generation=... id=...
  291. // ip=... network=... port=... priority=... protocol=... type=.../>
  292. if (line.indexOf('candidate:') === 0) {
  293. // eslint-disable-next-line no-param-reassign
  294. line = `a=${line}`;
  295. } else if (line.substring(0, 12) != 'a=candidate:') {
  296. logger.log(
  297. 'parseCandidate called with a line that is not a candidate'
  298. + ' line');
  299. logger.log(line);
  300. return null;
  301. }
  302. if (line.substring(line.length - 2) == '\r\n') { // chomp it
  303. // eslint-disable-next-line no-param-reassign
  304. line = line.substring(0, line.length - 2);
  305. }
  306. const candidate = {};
  307. const elems = line.split(' ');
  308. if (elems[6] != 'typ') {
  309. logger.log('did not find typ in the right place');
  310. logger.log(line);
  311. return null;
  312. }
  313. candidate.foundation = elems[0].substring(12);
  314. candidate.component = elems[1];
  315. candidate.protocol = elems[2].toLowerCase();
  316. candidate.priority = elems[3];
  317. candidate.ip = elems[4];
  318. candidate.port = elems[5];
  319. // elems[6] => "typ"
  320. candidate.type = elems[7];
  321. candidate.generation = '0'; // default, may be overwritten below
  322. for (let i = 8; i < elems.length; i += 2) {
  323. switch (elems[i]) {
  324. case 'raddr':
  325. candidate['rel-addr'] = elems[i + 1];
  326. break;
  327. case 'rport':
  328. candidate['rel-port'] = elems[i + 1];
  329. break;
  330. case 'generation':
  331. candidate.generation = elems[i + 1];
  332. break;
  333. case 'tcptype':
  334. candidate.tcptype = elems[i + 1];
  335. break;
  336. default: // TODO
  337. logger.log(`not translating "${elems[i]}" = "${elems[i + 1]}"`);
  338. }
  339. }
  340. candidate.network = '1';
  341. // not applicable to SDP -- FIXME: should be unique, not just random
  342. // eslint-disable-next-line newline-per-chained-call
  343. candidate.id = Math.random().toString(36).substr(2, 10);
  344. return candidate;
  345. },
  346. candidateFromJingle(cand) {
  347. let line = 'a=candidate:';
  348. line += cand.getAttribute('foundation');
  349. line += ' ';
  350. line += cand.getAttribute('component');
  351. line += ' ';
  352. let protocol = cand.getAttribute('protocol');
  353. // use tcp candidates for FF
  354. if (RTCBrowserType.isFirefox() && protocol.toLowerCase() == 'ssltcp') {
  355. protocol = 'tcp';
  356. }
  357. line += protocol; // .toUpperCase(); // chrome M23 doesn't like this
  358. line += ' ';
  359. line += cand.getAttribute('priority');
  360. line += ' ';
  361. line += cand.getAttribute('ip');
  362. line += ' ';
  363. line += cand.getAttribute('port');
  364. line += ' ';
  365. line += 'typ';
  366. line += ` ${cand.getAttribute('type')}`;
  367. line += ' ';
  368. switch (cand.getAttribute('type')) {
  369. case 'srflx':
  370. case 'prflx':
  371. case 'relay':
  372. if (cand.getAttribute('rel-addr')
  373. && cand.getAttribute('rel-port')) {
  374. line += 'raddr';
  375. line += ' ';
  376. line += cand.getAttribute('rel-addr');
  377. line += ' ';
  378. line += 'rport';
  379. line += ' ';
  380. line += cand.getAttribute('rel-port');
  381. line += ' ';
  382. }
  383. break;
  384. }
  385. if (protocol.toLowerCase() == 'tcp') {
  386. line += 'tcptype';
  387. line += ' ';
  388. line += cand.getAttribute('tcptype');
  389. line += ' ';
  390. }
  391. line += 'generation';
  392. line += ' ';
  393. line += cand.getAttribute('generation') || '0';
  394. return `${line}\r\n`;
  395. },
  396. /**
  397. * Parse the 'most' primary video ssrc from the given m line
  398. * @param {object} mLine object as parsed from transform.parse
  399. * @return {number} the primary video ssrc from the given m line
  400. */
  401. parsePrimaryVideoSsrc(videoMLine) {
  402. const numSsrcs = videoMLine.ssrcs
  403. .map(ssrcInfo => ssrcInfo.id)
  404. .filter((ssrc, index, array) => array.indexOf(ssrc) === index)
  405. .length;
  406. const numGroups
  407. = (videoMLine.ssrcGroups && videoMLine.ssrcGroups.length) || 0;
  408. if (numSsrcs > 1 && numGroups === 0) {
  409. // Ambiguous, can't figure out the primary
  410. return;
  411. }
  412. let primarySsrc = null;
  413. if (numSsrcs === 1) {
  414. primarySsrc = videoMLine.ssrcs[0].id;
  415. } else if (numSsrcs === 2) {
  416. // Can figure it out if there's an FID group
  417. const fidGroup
  418. = videoMLine.ssrcGroups.find(
  419. group => group.semantics === 'FID');
  420. if (fidGroup) {
  421. primarySsrc = fidGroup.ssrcs.split(' ')[0];
  422. }
  423. } else if (numSsrcs >= 3) {
  424. // Can figure it out if there's a sim group
  425. const simGroup
  426. = videoMLine.ssrcGroups.find(
  427. group => group.semantics === 'SIM');
  428. if (simGroup) {
  429. primarySsrc = simGroup.ssrcs.split(' ')[0];
  430. }
  431. }
  432. return primarySsrc;
  433. },
  434. /**
  435. * Generate an ssrc
  436. * @returns {number} an ssrc
  437. */
  438. generateSsrc() {
  439. return RandomUtil.randomInt(1, 0xffffffff);
  440. },
  441. /**
  442. * Get an attribute for the given ssrc with the given attributeName
  443. * from the given mline
  444. * @param {object} mLine an mLine object as parsed from transform.parse
  445. * @param {number} ssrc the ssrc for which an attribtue is desired
  446. * @param {string} attributeName the name of the desired attribute
  447. * @returns {string} the value corresponding to the given ssrc
  448. * and attributeName
  449. */
  450. getSsrcAttribute(mLine, ssrc, attributeName) {
  451. for (let i = 0; i < mLine.ssrcs.length; ++i) {
  452. const ssrcLine = mLine.ssrcs[i];
  453. if (ssrcLine.id === ssrc
  454. && ssrcLine.attribute === attributeName) {
  455. return ssrcLine.value;
  456. }
  457. }
  458. },
  459. /**
  460. * Parses the ssrcs from the group sdp line and
  461. * returns them as a list of numbers
  462. * @param {object} the ssrcGroup object as parsed from
  463. * sdp-transform
  464. * @returns {list<number>} a list of the ssrcs in the group
  465. * parsed as numbers
  466. */
  467. parseGroupSsrcs(ssrcGroup) {
  468. return ssrcGroup
  469. .ssrcs
  470. .split(' ')
  471. .map(ssrcStr => parseInt(ssrcStr));
  472. },
  473. /**
  474. * Get the mline of the given type from the given sdp
  475. * @param {object} sdp sdp as parsed from transform.parse
  476. * @param {string} type the type of the desired mline (e.g. "video")
  477. * @returns {object} a media object
  478. */
  479. getMedia(sdp, type) {
  480. return sdp.media.find(m => m.type === type);
  481. },
  482. /**
  483. * Sets the given codecName as the preferred codec by
  484. * moving it to the beginning of the payload types
  485. * list (modifies the given mline in place). If there
  486. * are multiple options within the same codec (multiple h264
  487. * profiles, for instance), this will prefer the first one
  488. * that is found.
  489. * @param {object} videoMLine the video mline object from
  490. * an sdp as parsed by transform.parse
  491. * @param {string} the name of the preferred codec
  492. */
  493. preferVideoCodec(videoMLine, codecName) {
  494. let payloadType = null;
  495. for (let i = 0; i < videoMLine.rtp.length; ++i) {
  496. const rtp = videoMLine.rtp[i];
  497. if (rtp.codec === codecName) {
  498. payloadType = rtp.payload;
  499. break;
  500. }
  501. }
  502. if (payloadType) {
  503. const payloadTypes
  504. = videoMLine.payloads.split(' ').map(p => parseInt(p));
  505. const payloadIndex = payloadTypes.indexOf(payloadType);
  506. payloadTypes.splice(payloadIndex, 1);
  507. payloadTypes.unshift(payloadType);
  508. videoMLine.payloads = payloadTypes.join(' ');
  509. }
  510. }
  511. };
  512. module.exports = SDPUtil;