Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

SDPUtil.js 21KB

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