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.

SDP.js 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. import $ from 'jquery';
  2. import { cloneDeep } from 'lodash-es';
  3. import transform from 'sdp-transform';
  4. import { Strophe } from 'strophe.js';
  5. import { MediaDirection } from '../../service/RTC/MediaDirection';
  6. import { MediaType } from '../../service/RTC/MediaType';
  7. import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
  8. import browser from '../browser';
  9. import SDPUtil from './SDPUtil';
  10. /**
  11. * A class that translates the Jingle messages received from the signaling server into SDP format that the
  12. * browser understands and vice versa. This is needed for media session establishment and for signaling local and
  13. * remote sources across peers.
  14. */
  15. export default class SDP {
  16. /**
  17. * Constructor.
  18. *
  19. * @param {string} sdp - The SDP generated by the browser when SDP->Jingle conversion is needed, an empty string
  20. * when Jingle->SDP conversion is needed.
  21. * @param {boolean} isP2P - Whether this SDP belongs to a p2p peerconnection.
  22. */
  23. constructor(sdp, isP2P = false) {
  24. const media = sdp.split('\r\nm=');
  25. for (let i = 1, length = media.length; i < length; i++) {
  26. let mediaI = `m=${media[i]}`;
  27. if (i !== length - 1) {
  28. mediaI += '\r\n';
  29. }
  30. media[i] = mediaI;
  31. }
  32. const session = `${media.shift()}\r\n`;
  33. this.isP2P = isP2P;
  34. this.media = media;
  35. this.raw = session + media.join('');
  36. this.session = session;
  37. // This flag will make {@link transportToJingle} and {@link jingle2media} replace ICE candidates IPs with
  38. // invalid value of '1.1.1.1' which will cause ICE failure. The flag is used in the automated testing.
  39. this.failICE = false;
  40. // Whether or not to remove TCP ice candidates when translating from/to jingle.
  41. this.removeTcpCandidates = false;
  42. // Whether or not to remove UDP ice candidates when translating from/to jingle.
  43. this.removeUdpCandidates = false;
  44. }
  45. /**
  46. * Adds a new m-line to the description so that a new local source can then be attached to the transceiver that gets
  47. * added after a reneogtiation cycle.
  48. *
  49. * @param {MediaType} mediaType media type of the new source that is being added.
  50. * @returns {void}
  51. */
  52. addMlineForNewLocalSource(mediaType) {
  53. const mid = this.media.length;
  54. const sdp = transform.parse(this.raw);
  55. const mline = cloneDeep(sdp.media.find(m => m.type === mediaType));
  56. // Edit media direction, mid and remove the existing ssrc lines in the m-line.
  57. mline.mid = mid;
  58. mline.direction = MediaDirection.RECVONLY;
  59. mline.msid = undefined;
  60. mline.ssrcs = undefined;
  61. mline.ssrcGroups = undefined;
  62. // We regenerate the BUNDLE group (since we added a new m-line).
  63. sdp.media = [ ...sdp.media, mline ];
  64. sdp.groups.forEach(group => {
  65. if (group.type === 'BUNDLE') {
  66. group.mids = [ ...group.mids.split(' '), mid ].join(' ');
  67. }
  68. });
  69. this.raw = transform.write(sdp);
  70. }
  71. /**
  72. * Checks if a given SSRC is present in the SDP.
  73. *
  74. * @param {string} ssrc
  75. * @returns {boolean}
  76. */
  77. containsSSRC(ssrc) {
  78. const sourceMap = this.getMediaSsrcMap();
  79. return [ ...sourceMap.values() ].some(media => media.ssrcs[ssrc]);
  80. }
  81. /**
  82. * Converts the Jingle message element to SDP.
  83. *
  84. * @param {*} jingle - The Jingle message element.
  85. * @returns {void}
  86. */
  87. fromJingle(jingle) {
  88. const sessionId = Date.now();
  89. // Use a unique session id for every TPC.
  90. this.raw = 'v=0\r\n'
  91. + `o=- ${sessionId} 2 IN IP4 0.0.0.0\r\n`
  92. + 's=-\r\n'
  93. + 't=0 0\r\n';
  94. const groups = $(jingle).find(`>group[xmlns='${XEP.BUNDLE_MEDIA}']`);
  95. if (groups.length) {
  96. groups.each((idx, group) => {
  97. const contents = $(group)
  98. .find('>content')
  99. .map((_, content) => content.getAttribute('name'))
  100. .get();
  101. if (contents.length > 0) {
  102. this.raw
  103. += `a=group:${
  104. group.getAttribute('semantics')
  105. || group.getAttribute('type')} ${
  106. contents.join(' ')}\r\n`;
  107. }
  108. });
  109. }
  110. this.session = this.raw;
  111. jingle.find('>content').each((_, content) => {
  112. const m = this.jingle2media($(content));
  113. this.media.push(m);
  114. });
  115. this.raw = this.session + this.media.join('');
  116. }
  117. /**
  118. * Returns an SSRC Map by extracting SSRCs and SSRC groups from all the m-lines in the SDP.
  119. *
  120. * @returns {*}
  121. */
  122. getMediaSsrcMap() {
  123. const sourceInfo = new Map();
  124. this.media.forEach((mediaItem, mediaindex) => {
  125. const mid = SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:'));
  126. const mline = SDPUtil.parseMLine(mediaItem.split('\r\n')[0]);
  127. const media = {
  128. mediaindex,
  129. mediaType: mline.media,
  130. mid,
  131. ssrcs: {},
  132. ssrcGroups: []
  133. };
  134. SDPUtil.findLines(mediaItem, 'a=ssrc:').forEach(line => {
  135. const linessrc = line.substring(7).split(' ')[0];
  136. // Allocate new ChannelSsrc.
  137. if (!media.ssrcs[linessrc]) {
  138. media.ssrcs[linessrc] = {
  139. ssrc: linessrc,
  140. lines: []
  141. };
  142. }
  143. media.ssrcs[linessrc].lines.push(line);
  144. });
  145. SDPUtil.findLines(mediaItem, 'a=ssrc-group:').forEach(line => {
  146. const idx = line.indexOf(' ');
  147. const semantics = line.substr(0, idx).substr(13);
  148. const ssrcs = line.substr(14 + semantics.length).split(' ');
  149. if (ssrcs.length) {
  150. media.ssrcGroups.push({
  151. semantics,
  152. ssrcs
  153. });
  154. }
  155. });
  156. sourceInfo.set(mediaindex, media);
  157. });
  158. return sourceInfo;
  159. }
  160. /**
  161. * Converts the content section from Jingle to a media section that can be appended to the SDP.
  162. *
  163. * @param {*} content - The content section from the Jingle message element.
  164. * @returns {*} - The constructed media sections.
  165. */
  166. jingle2media(content) {
  167. const desc = content.find('>description');
  168. const transport = content.find(`>transport[xmlns='${XEP.ICE_UDP_TRANSPORT}']`);
  169. let sdp = '';
  170. const sctp = transport.find(`>sctpmap[xmlns='${XEP.SCTP_DATA_CHANNEL}']`);
  171. const media = { media: desc.attr('media') };
  172. media.port = '9';
  173. if (content.attr('senders') === 'rejected') {
  174. media.port = '0';
  175. }
  176. if (transport.find(`>fingerprint[xmlns='${XEP.DTLS_SRTP}']`).length) {
  177. media.proto = sctp.length ? 'UDP/DTLS/SCTP' : 'UDP/TLS/RTP/SAVPF';
  178. } else {
  179. media.proto = 'UDP/TLS/RTP/SAVPF';
  180. }
  181. if (sctp.length) {
  182. sdp += `m=application ${media.port} UDP/DTLS/SCTP webrtc-datachannel\r\n`;
  183. sdp += `a=sctp-port:${sctp.attr('number')}\r\n`;
  184. sdp += 'a=max-message-size:262144\r\n';
  185. } else {
  186. media.fmt
  187. = desc
  188. .find('>payload-type')
  189. .map((_, payloadType) => payloadType.getAttribute('id'))
  190. .get();
  191. sdp += `${SDPUtil.buildMLine(media)}\r\n`;
  192. }
  193. sdp += 'c=IN IP4 0.0.0.0\r\n';
  194. if (!sctp.length) {
  195. sdp += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
  196. }
  197. if (transport.length) {
  198. if (transport.attr('ufrag')) {
  199. sdp += `${SDPUtil.buildICEUfrag(transport.attr('ufrag'))}\r\n`;
  200. }
  201. if (transport.attr('pwd')) {
  202. sdp += `${SDPUtil.buildICEPwd(transport.attr('pwd'))}\r\n`;
  203. }
  204. transport.find(`>fingerprint[xmlns='${XEP.DTLS_SRTP}']`).each((_, fingerprint) => {
  205. sdp += `a=fingerprint:${fingerprint.getAttribute('hash')} ${$(fingerprint).text()}\r\n`;
  206. if (fingerprint.hasAttribute('setup')) {
  207. sdp += `a=setup:${fingerprint.getAttribute('setup')}\r\n`;
  208. }
  209. });
  210. }
  211. transport.find('>candidate').each((_, candidate) => {
  212. let protocol = candidate.getAttribute('protocol');
  213. protocol = typeof protocol === 'string' ? protocol.toLowerCase() : '';
  214. if ((this.removeTcpCandidates && (protocol === 'tcp' || protocol === 'ssltcp'))
  215. || (this.removeUdpCandidates && protocol === 'udp')) {
  216. return;
  217. } else if (this.failICE) {
  218. candidate.setAttribute('ip', '1.1.1.1');
  219. }
  220. sdp += SDPUtil.candidateFromJingle(candidate);
  221. });
  222. switch (content.attr('senders')) {
  223. case 'initiator':
  224. sdp += `a=${MediaDirection.SENDONLY}\r\n`;
  225. break;
  226. case 'responder':
  227. sdp += `a=${MediaDirection.RECVONLY}\r\n`;
  228. break;
  229. case 'none':
  230. sdp += `a=${MediaDirection.INACTIVE}\r\n`;
  231. break;
  232. case 'both':
  233. sdp += `a=${MediaDirection.SENDRECV}\r\n`;
  234. break;
  235. }
  236. sdp += `a=mid:${content.attr('name')}\r\n`;
  237. // <description><rtcp-mux/></description>
  238. // see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
  239. // and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
  240. if (desc.find('>rtcp-mux').length) {
  241. sdp += 'a=rtcp-mux\r\n';
  242. }
  243. desc.find('>payload-type').each((_, payloadType) => {
  244. sdp += `${SDPUtil.buildRTPMap(payloadType)}\r\n`;
  245. if ($(payloadType).find('>parameter').length) {
  246. sdp += `a=fmtp:${payloadType.getAttribute('id')} `;
  247. sdp += $(payloadType)
  248. .find('>parameter')
  249. .map((__, parameter) => {
  250. const name = parameter.getAttribute('name');
  251. return (name ? `${name}=` : '') + parameter.getAttribute('value');
  252. })
  253. .get()
  254. .join(';');
  255. sdp += '\r\n';
  256. }
  257. sdp += this.rtcpFbFromJingle($(payloadType), payloadType.getAttribute('id'));
  258. });
  259. sdp += this.rtcpFbFromJingle(desc, '*');
  260. desc.find(`>rtp-hdrext[xmlns='${XEP.RTP_HEADER_EXTENSIONS}']`).each((_, hdrExt) => {
  261. sdp += `a=extmap:${hdrExt.getAttribute('id')} ${hdrExt.getAttribute('uri')}\r\n`;
  262. });
  263. if (desc.find(`>extmap-allow-mixed[xmlns='${XEP.RTP_HEADER_EXTENSIONS}']`).length > 0) {
  264. sdp += 'a=extmap-allow-mixed\r\n';
  265. }
  266. desc
  267. .find(`>ssrc-group[xmlns='${XEP.SOURCE_ATTRIBUTES}']`)
  268. .each((_, ssrcGroup) => {
  269. const semantics = ssrcGroup.getAttribute('semantics');
  270. const ssrcs
  271. = $(ssrcGroup)
  272. .find('>source')
  273. .map((__, source) => source.getAttribute('ssrc'))
  274. .get();
  275. if (ssrcs.length) {
  276. sdp += `a=ssrc-group:${semantics} ${ssrcs.join(' ')}\r\n`;
  277. }
  278. });
  279. let userSources = '';
  280. let nonUserSources = '';
  281. desc
  282. .find(`>source[xmlns='${XEP.SOURCE_ATTRIBUTES}']`)
  283. .each((_, source) => {
  284. const ssrc = source.getAttribute('ssrc');
  285. let isUserSource = true;
  286. let sourceStr = '';
  287. $(source)
  288. .find('>parameter')
  289. .each((__, parameter) => {
  290. const name = parameter.getAttribute('name');
  291. let value = parameter.getAttribute('value');
  292. value = SDPUtil.filterSpecialChars(value);
  293. sourceStr += `a=ssrc:${ssrc} ${name}`;
  294. if (value && value.length) {
  295. sourceStr += `:${value}`;
  296. }
  297. sourceStr += '\r\n';
  298. if (value?.includes('mixedmslabel')) {
  299. isUserSource = false;
  300. }
  301. });
  302. if (isUserSource) {
  303. userSources += sourceStr;
  304. } else {
  305. nonUserSources += sourceStr;
  306. }
  307. });
  308. // Append sources in the correct order, the mixedmslable m-line which has the JVB's SSRC for RTCP termination
  309. // is expected to be in the first m-line.
  310. sdp += nonUserSources + userSources;
  311. return sdp;
  312. }
  313. /**
  314. * Coverts the RTCP attributes for the session from XMPP format to SDP.
  315. * https://xmpp.org/extensions/xep-0293.html
  316. *
  317. * @param {*} elem - Jingle message element.
  318. * @param {*} payloadtype - Payload type for the codec.
  319. * @returns {string}
  320. */
  321. rtcpFbFromJingle(elem, payloadtype) {
  322. let sdp = '';
  323. const feedbackElementTrrInt = elem.find(`>rtcp-fb-trr-int[xmlns='${XEP.RTP_FEEDBACK}']`);
  324. if (feedbackElementTrrInt.length) {
  325. sdp += 'a=rtcp-fb:* trr-int ';
  326. sdp += feedbackElementTrrInt.attr('value') || '0';
  327. sdp += '\r\n';
  328. }
  329. const feedbackElements = elem.find(`>rtcp-fb[xmlns='${XEP.RTP_FEEDBACK}']`);
  330. feedbackElements.each((_, fb) => {
  331. sdp += `a=rtcp-fb:${payloadtype} ${fb.getAttribute('type')}`;
  332. if (fb.hasAttribute('subtype')) {
  333. sdp += ` ${fb.getAttribute('subtype')}`;
  334. }
  335. sdp += '\r\n';
  336. });
  337. return sdp;
  338. }
  339. /**
  340. * Converts the RTCP attributes for the session from SDP to XMPP format.
  341. * https://xmpp.org/extensions/xep-0293.html
  342. *
  343. * @param {*} mediaIndex - The index of the media section in the SDP.
  344. * @param {*} elem - The Jingle message element.
  345. * @param {*} payloadtype - payload type for the codec.
  346. */
  347. rtcpFbToJingle(mediaIndex, elem, payloadtype) {
  348. const lines = SDPUtil.findLines(this.media[mediaIndex], `a=rtcp-fb:${payloadtype}`);
  349. lines.forEach(line => {
  350. const feedback = SDPUtil.parseRTCPFB(line);
  351. if (feedback.type === 'trr-int') {
  352. elem.c('rtcp-fb-trr-int', {
  353. xmlns: XEP.RTP_FEEDBACK,
  354. value: feedback.params[0]
  355. });
  356. elem.up();
  357. } else {
  358. elem.c('rtcp-fb', {
  359. xmlns: XEP.RTP_FEEDBACK,
  360. type: feedback.type
  361. });
  362. if (feedback.params.length > 0) {
  363. elem.attrs({ 'subtype': feedback.params[0] });
  364. }
  365. elem.up();
  366. }
  367. });
  368. }
  369. /**
  370. * Converts the current SDP to a Jingle message that can be sent over the wire to a signaling server.
  371. *
  372. * @param {*} elem - The Jingle message element.
  373. * @param {*} thecreator - Sender role, whether it is an 'initiator' or 'responder'.
  374. * @returns - The updated Jingle message element.
  375. */
  376. toJingle(elem, thecreator) {
  377. SDPUtil.findLines(this.session, 'a=group:').forEach(line => {
  378. const parts = line.split(' ');
  379. const semantics = parts.shift().substr(8);
  380. elem.c('group', {
  381. xmlns: XEP.BUNDLE_MEDIA,
  382. semantics
  383. });
  384. // Bundle all the media types. Jicofo expects the 'application' media type to be signaled as 'data'.
  385. let mediaTypes = [ MediaType.AUDIO, MediaType.VIDEO, 'data' ];
  386. // For p2p connection, 'mid' will be used in the bundle group.
  387. if (this.isP2P) {
  388. mediaTypes = this.media.map(mediaItem => SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:')));
  389. }
  390. mediaTypes.forEach(type => elem.c('content', { name: type }).up());
  391. elem.up();
  392. });
  393. this.media.forEach((mediaItem, i) => {
  394. const mline = SDPUtil.parseMLine(mediaItem.split('\r\n')[0]);
  395. const mediaType = mline.media === MediaType.APPLICATION ? 'data' : mline.media;
  396. let ssrc = false;
  397. const assrcline = SDPUtil.findLine(mediaItem, 'a=ssrc:');
  398. if (assrcline) {
  399. ssrc = assrcline.substring(7).split(' ')[0];
  400. }
  401. const contents = $(elem.tree()).find(`content[name='${mediaType}']`);
  402. // Append source groups from the new m-lines to the existing media description. The SDP will have multiple
  403. // m-lines for audio and video including the recv-only ones for remote sources but there needs to be only
  404. // one media description for a given media type that should include all the sources, i.e., both the camera
  405. // and screenshare sources should be added to the 'video' description.
  406. for (const content of contents) {
  407. if (!content.hasAttribute('creator')) {
  408. // eslint-disable-next-line no-continue
  409. continue;
  410. }
  411. if (ssrc) {
  412. const description = $(content).find('description');
  413. const ssrcMap = SDPUtil.parseSSRC(mediaItem);
  414. for (const [ availableSsrc, ssrcParameters ] of ssrcMap) {
  415. const sourceName = SDPUtil.parseSourceNameLine(ssrcParameters);
  416. const videoType = SDPUtil.parseVideoTypeLine(ssrcParameters);
  417. const source = Strophe.xmlElement('source', {
  418. ssrc: availableSsrc,
  419. name: sourceName,
  420. videoType,
  421. xmlns: XEP.SOURCE_ATTRIBUTES
  422. });
  423. const msid = SDPUtil.parseMSIDAttribute(ssrcParameters);
  424. if (msid) {
  425. const param = Strophe.xmlElement('parameter', {
  426. name: 'msid',
  427. value: msid
  428. });
  429. source.append(param);
  430. }
  431. description.append(source);
  432. }
  433. const ssrcGroupLines = SDPUtil.findLines(mediaItem, 'a=ssrc-group:');
  434. ssrcGroupLines.forEach(line => {
  435. const { semantics, ssrcs } = SDPUtil.parseSSRCGroupLine(line);
  436. if (ssrcs.length) {
  437. const group = Strophe.xmlElement('ssrc-group', {
  438. semantics,
  439. xmlns: XEP.SOURCE_ATTRIBUTES
  440. });
  441. for (const val of ssrcs) {
  442. const src = Strophe.xmlElement('source', {
  443. ssrc: val
  444. });
  445. group.append(src);
  446. }
  447. description.append(group);
  448. }
  449. });
  450. }
  451. return;
  452. }
  453. const mid = SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:'));
  454. elem.c('content', {
  455. creator: thecreator,
  456. name: this.isP2P ? mid : mediaType
  457. });
  458. if (mediaType === MediaType.VIDEO && typeof this.initialLastN === 'number') {
  459. elem.c('initial-last-n', {
  460. xmlns: 'jitsi:colibri2',
  461. value: this.initialLastN
  462. }).up();
  463. }
  464. if ([ MediaType.AUDIO, MediaType.VIDEO ].includes(mediaType)) {
  465. elem.c('description', {
  466. xmlns: XEP.RTP_MEDIA,
  467. media: mediaType
  468. });
  469. mline.fmt.forEach(format => {
  470. const rtpmap = SDPUtil.findLine(mediaItem, `a=rtpmap:${format}`);
  471. elem.c('payload-type', SDPUtil.parseRTPMap(rtpmap));
  472. const afmtpline = SDPUtil.findLine(mediaItem, `a=fmtp:${format}`);
  473. if (afmtpline) {
  474. const fmtpParameters = SDPUtil.parseFmtp(afmtpline);
  475. fmtpParameters.forEach(param => elem.c('parameter', param).up());
  476. }
  477. this.rtcpFbToJingle(i, elem, format);
  478. elem.up();
  479. });
  480. if (ssrc) {
  481. const ssrcMap = SDPUtil.parseSSRC(mediaItem);
  482. for (const [ availableSsrc, ssrcParameters ] of ssrcMap) {
  483. const sourceName = SDPUtil.parseSourceNameLine(ssrcParameters);
  484. const videoType = SDPUtil.parseVideoTypeLine(ssrcParameters);
  485. elem.c('source', {
  486. ssrc: availableSsrc,
  487. name: sourceName,
  488. videoType,
  489. xmlns: XEP.SOURCE_ATTRIBUTES
  490. });
  491. const msid = SDPUtil.parseMSIDAttribute(ssrcParameters);
  492. if (msid) {
  493. elem.c('parameter').attrs({
  494. name: 'msid',
  495. value: msid
  496. });
  497. elem.up();
  498. }
  499. elem.up();
  500. }
  501. const ssrcGroupLines = SDPUtil.findLines(mediaItem, 'a=ssrc-group:');
  502. ssrcGroupLines.forEach(line => {
  503. const { semantics, ssrcs } = SDPUtil.parseSSRCGroupLine(line);
  504. if (ssrcs.length) {
  505. elem.c('ssrc-group', {
  506. semantics,
  507. xmlns: XEP.SOURCE_ATTRIBUTES
  508. });
  509. ssrcs.forEach(s => elem.c('source', { ssrc: s }).up());
  510. elem.up();
  511. }
  512. });
  513. }
  514. const ridLines = SDPUtil.findLines(mediaItem, 'a=rid:');
  515. if (ridLines.length && browser.usesRidsForSimulcast()) {
  516. // Map a line which looks like "a=rid:2 send" to just the rid ("2").
  517. const rids = ridLines.map(ridLine => ridLine.split(':')[1].split(' ')[0]);
  518. rids.forEach(rid => {
  519. elem.c('source', {
  520. rid,
  521. xmlns: XEP.SOURCE_ATTRIBUTES
  522. });
  523. elem.up();
  524. });
  525. const unifiedSimulcast = SDPUtil.findLine(mediaItem, 'a=simulcast:');
  526. if (unifiedSimulcast) {
  527. elem.c('rid-group', {
  528. semantics: 'SIM',
  529. xmlns: XEP.SOURCE_ATTRIBUTES
  530. });
  531. rids.forEach(rid => elem.c('source', { rid }).up());
  532. elem.up();
  533. }
  534. }
  535. if (SDPUtil.findLine(mediaItem, 'a=rtcp-mux')) {
  536. elem.c('rtcp-mux').up();
  537. }
  538. this.rtcpFbToJingle(i, elem, '*');
  539. const extmapLines = SDPUtil.findLines(mediaItem, 'a=extmap:', this.session);
  540. extmapLines.forEach(extmapLine => {
  541. const extmap = SDPUtil.parseExtmap(extmapLine);
  542. elem.c('rtp-hdrext', {
  543. xmlns: XEP.RTP_HEADER_EXTENSIONS,
  544. uri: extmap.uri,
  545. id: extmap.value
  546. });
  547. if (extmap.hasOwnProperty('direction')) {
  548. switch (extmap.direction) {
  549. case MediaDirection.SENDONLY:
  550. elem.attrs({ senders: 'responder' });
  551. break;
  552. case MediaDirection.RECVONLY:
  553. elem.attrs({ senders: 'initiator' });
  554. break;
  555. case MediaDirection.SENDRECV:
  556. elem.attrs({ senders: 'both' });
  557. break;
  558. case MediaDirection.INACTIVE:
  559. elem.attrs({ senders: 'none' });
  560. break;
  561. }
  562. }
  563. elem.up();
  564. });
  565. if (SDPUtil.findLine(mediaItem, 'a=extmap-allow-mixed', this.session)) {
  566. elem.c('extmap-allow-mixed', {
  567. xmlns: XEP.RTP_HEADER_EXTENSIONS
  568. });
  569. elem.up();
  570. }
  571. elem.up(); // end of description
  572. }
  573. // Map ice-ufrag/pwd, dtls fingerprint, candidates.
  574. this.transportToJingle(i, elem);
  575. // Set senders attribute based on media direction
  576. if (SDPUtil.findLine(mediaItem, `a=${MediaDirection.SENDRECV}`)) {
  577. elem.attrs({ senders: 'both' });
  578. } else if (SDPUtil.findLine(mediaItem, `a=${MediaDirection.SENDONLY}`)) {
  579. elem.attrs({ senders: 'initiator' });
  580. } else if (SDPUtil.findLine(mediaItem, `a=${MediaDirection.RECVONLY}`)) {
  581. elem.attrs({ senders: 'responder' });
  582. } else if (SDPUtil.findLine(mediaItem, `a=${MediaDirection.INACTIVE}`)) {
  583. elem.attrs({ senders: 'none' });
  584. }
  585. // Reject an m-line only when port is 0 and a=bundle-only is not present in the section.
  586. // The port is automatically set to 0 when bundle-only is used.
  587. if (mline.port === '0' && !SDPUtil.findLine(mediaItem, 'a=bundle-only', this.session)) {
  588. elem.attrs({ senders: 'rejected' });
  589. }
  590. elem.up(); // end of content
  591. });
  592. elem.up();
  593. return elem;
  594. }
  595. /**
  596. * Converts the session transport information from SDP to XMPP format.
  597. *
  598. * @param {*} mediaIndex The index for the m-line in the SDP.
  599. * @param {*} elem The transport element.
  600. */
  601. transportToJingle(mediaIndex, elem) {
  602. elem.c('transport');
  603. const sctpport = SDPUtil.findLine(this.media[mediaIndex], 'a=sctp-port:', this.session);
  604. const sctpmap = SDPUtil.findLine(this.media[mediaIndex], 'a=sctpmap:', this.session);
  605. if (sctpport) {
  606. const sctpAttrs = SDPUtil.parseSCTPPort(sctpport);
  607. elem.c('sctpmap', {
  608. xmlns: XEP.SCTP_DATA_CHANNEL,
  609. number: sctpAttrs, // SCTP port
  610. protocol: 'webrtc-datachannel' // protocol
  611. });
  612. // The parser currently requires streams to be present.
  613. elem.attrs({ streams: 0 });
  614. elem.up();
  615. } else if (sctpmap) {
  616. const sctpAttrs = SDPUtil.parseSCTPMap(sctpmap);
  617. elem.c('sctpmap', {
  618. xmlns: XEP.SCTP_DATA_CHANNEL,
  619. number: sctpAttrs[0], // SCTP port
  620. protocol: sctpAttrs[1] // protocol
  621. });
  622. // Optional stream count attribute.
  623. elem.attrs({ streams: sctpAttrs.length > 2 ? sctpAttrs[2] : 0 });
  624. elem.up();
  625. }
  626. const fingerprints = SDPUtil.findLines(this.media[mediaIndex], 'a=fingerprint:', this.session);
  627. fingerprints.forEach(line => {
  628. const fingerprint = SDPUtil.parseFingerprint(line);
  629. fingerprint.xmlns = XEP.DTLS_SRTP;
  630. elem.c('fingerprint').t(fingerprint.fingerprint);
  631. delete fingerprint.fingerprint;
  632. const setupLine = SDPUtil.findLine(this.media[mediaIndex], 'a=setup:', this.session);
  633. if (setupLine) {
  634. fingerprint.setup = setupLine.substr(8);
  635. }
  636. elem.attrs(fingerprint);
  637. elem.up(); // end of fingerprint
  638. });
  639. const iceParameters = SDPUtil.iceparams(this.media[mediaIndex], this.session);
  640. if (iceParameters) {
  641. iceParameters.xmlns = XEP.ICE_UDP_TRANSPORT;
  642. elem.attrs(iceParameters);
  643. const candidateLines = SDPUtil.findLines(this.media[mediaIndex], 'a=candidate:', this.session);
  644. candidateLines.forEach(line => { // add any a=candidate lines
  645. const candidate = SDPUtil.candidateToJingle(line);
  646. if (this.failICE) {
  647. candidate.ip = '1.1.1.1';
  648. }
  649. const protocol = candidate && typeof candidate.protocol === 'string'
  650. ? candidate.protocol.toLowerCase() : '';
  651. if ((this.removeTcpCandidates && (protocol === 'tcp' || protocol === 'ssltcp'))
  652. || (this.removeUdpCandidates && protocol === 'udp')) {
  653. return;
  654. }
  655. elem.c('candidate', candidate).up();
  656. });
  657. }
  658. elem.up(); // end of transport
  659. }
  660. }