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

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