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

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