您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

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