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

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