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.

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