diff --git a/modules/sdp/SDP.js b/modules/sdp/SDP.js
index 87c48ee36d..80066832b8 100644
--- a/modules/sdp/SDP.js
+++ b/modules/sdp/SDP.js
@@ -3,803 +3,734 @@ import { cloneDeep } from 'lodash-es';
import transform from 'sdp-transform';
import { MediaDirection } from '../../service/RTC/MediaDirection';
+import { MediaType } from '../../service/RTC/MediaType';
+import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
import browser from '../browser';
import SDPUtil from './SDPUtil';
/**
- *
- * @param sdp
+ * A class that translates the Jingle messages received from the signaling server into SDP format that the
+ * browser understands and vice versa. This is needed for media session establishment and for signaling local and
+ * remote sources across peers.
*/
-export default function SDP(sdp) {
- const media = sdp.split('\r\nm=');
+export default class SDP {
+ /**
+ * Constructor.
+ *
+ * @param {string} sdp - The SDP generated by the browser when SDP->Jingle conversion is needed, an empty string
+ * when Jingle->SDP conversion is needed.
+ */
+ constructor(sdp) {
+ const media = sdp.split('\r\nm=');
- for (let i = 1, length = media.length; i < length; i++) {
- let mediaI = `m=${media[i]}`;
+ for (let i = 1, length = media.length; i < length; i++) {
+ let mediaI = `m=${media[i]}`;
- if (i !== length - 1) {
- mediaI += '\r\n';
+ if (i !== length - 1) {
+ mediaI += '\r\n';
+ }
+ media[i] = mediaI;
}
- media[i] = mediaI;
- }
- const session = `${media.shift()}\r\n`;
+ const session = `${media.shift()}\r\n`;
- this.media = media;
- this.raw = session + media.join('');
- this.session = session;
-}
+ this.media = media;
+ this.raw = session + media.join('');
+ this.session = session;
-/**
- * A flag will make {@link transportToJingle} and {@link jingle2media} replace
- * ICE candidates IPs with invalid value of '1.1.1.1' which will cause ICE
- * failure. The flag is used in the automated testing.
- * @type {boolean}
- */
-SDP.prototype.failICE = false;
+ // This flag will make {@link transportToJingle} and {@link jingle2media} replace ICE candidates IPs with
+ // invalid value of '1.1.1.1' which will cause ICE failure. The flag is used in the automated testing.
+ this.failICE = false;
-/**
- * Whether or not to remove TCP ice candidates when translating from/to jingle.
- * @type {boolean}
- */
-SDP.prototype.removeTcpCandidates = false;
+ // Whether or not to remove TCP ice candidates when translating from/to jingle.
+ this.removeTcpCandidates = false;
-/**
- * Whether or not to remove UDP ice candidates when translating from/to jingle.
- * @type {boolean}
- */
-SDP.prototype.removeUdpCandidates = false;
+ // Whether or not to remove UDP ice candidates when translating from/to jingle.
+ this.removeUdpCandidates = false;
+ }
-/**
- * Adds a new m-line to the description so that a new local source can then be attached to the transceiver that gets
- * added after a reneogtiation cycle.
- *
- * @param {MediaType} mediaType media type of the new source that is being added.
- */
-SDP.prototype.addMlineForNewLocalSource = function(mediaType) {
- const mid = this.media.length;
- const sdp = transform.parse(this.raw);
- const mline = cloneDeep(sdp.media.find(m => m.type === mediaType));
+ /**
+ * Adds a new m-line to the description so that a new local source can then be attached to the transceiver that gets
+ * added after a reneogtiation cycle.
+ *
+ * @param {MediaType} mediaType media type of the new source that is being added.
+ * @returns {void}
+ */
+ addMlineForNewLocalSource(mediaType) {
+ const mid = this.media.length;
+ const sdp = transform.parse(this.raw);
+ const mline = cloneDeep(sdp.media.find(m => m.type === mediaType));
+
+ // Edit media direction, mid and remove the existing ssrc lines in the m-line.
+ mline.mid = mid;
+ mline.direction = MediaDirection.RECVONLY;
+ mline.msid = undefined;
+ mline.ssrcs = undefined;
+ mline.ssrcGroups = undefined;
+
+ // We regenerate the BUNDLE group (since we added a new m-line).
+ sdp.media = [ ...sdp.media, mline ];
+
+ sdp.groups.forEach(group => {
+ if (group.type === 'BUNDLE') {
+ group.mids = [ ...group.mids.split(' '), mid ].join(' ');
+ }
+ });
+ this.raw = transform.write(sdp);
+ }
+
+ /**
+ * Checks if a given SSRC is present in the SDP.
+ *
+ * @param {string} ssrc
+ * @returns {boolean}
+ */
+ containsSSRC(ssrc) {
+ const souceMap = this.getMediaSsrcMap();
+
+ return Object.values(souceMap).some(media => media.ssrcs[ssrc]);
+ }
- // Edit media direction, mid and remove the existing ssrc lines in the m-line.
- mline.mid = mid;
- mline.direction = MediaDirection.RECVONLY;
+ /**
+ * Converts the Jingle message element to SDP.
+ *
+ * @param {*} jingle - The Jingle message element.
+ * @returns {void}
+ */
+ fromJingle(jingle) {
+ const sessionId = Date.now();
- // Remove the ssrcs and source groups.
- mline.msid = undefined;
- mline.ssrcs = undefined;
- mline.ssrcGroups = undefined;
+ // Use a unique session id for every TPC.
+ this.raw = 'v=0\r\n'
+ + `o=- ${sessionId} 2 IN IP4 0.0.0.0\r\n`
+ + 's=-\r\n'
+ + 't=0 0\r\n';
- sdp.media = sdp.media.concat(mline);
+ const groups = $(jingle).find(`>group[xmlns='${XEP.BUNDLE_MEDIA}']`);
- // We regenerate the BUNDLE group (since we added a new m-line)
- sdp.groups.forEach(group => {
- if (group.type === 'BUNDLE') {
- const mids = group.mids.split(' ');
+ if (groups.length) {
+ groups.each((idx, group) => {
+ const contents = $(group)
+ .find('>content')
+ .map((_, content) => content.getAttribute('name'))
+ .get();
- mids.push(mid);
- group.mids = mids.join(' ');
+ if (contents.length > 0) {
+ this.raw
+ += `a=group:${
+ group.getAttribute('semantics')
+ || group.getAttribute('type')} ${
+ contents.join(' ')}\r\n`;
+ }
+ });
}
- });
- this.raw = transform.write(sdp);
-};
-/**
- * Returns map of MediaChannel mapped per channel idx.
- */
-SDP.prototype.getMediaSsrcMap = function() {
- const mediaSSRCs = {};
-
- for (let mediaindex = 0; mediaindex < this.media.length; mediaindex++) {
- const mid
- = SDPUtil.parseMID(
- SDPUtil.findLine(this.media[mediaindex], 'a=mid:'));
- const media = {
- mediaindex,
- mid,
- ssrcs: {},
- ssrcGroups: []
- };
-
- mediaSSRCs[mediaindex] = media;
-
- SDPUtil.findLines(this.media[mediaindex], 'a=ssrc:').forEach(line => {
- const linessrc = line.substring(7).split(' ')[0];
-
- // allocate new ChannelSsrc
-
- if (!media.ssrcs[linessrc]) {
- media.ssrcs[linessrc] = {
- ssrc: linessrc,
- lines: []
- };
- }
- media.ssrcs[linessrc].lines.push(line);
- });
- SDPUtil.findLines(this.media[mediaindex], 'a=ssrc-group:').forEach(line => {
- const idx = line.indexOf(' ');
- const semantics = line.substr(0, idx).substr(13);
- const ssrcs = line.substr(14 + semantics.length).split(' ');
-
- if (ssrcs.length) {
- media.ssrcGroups.push({
- semantics,
- ssrcs
- });
- }
+ this.session = this.raw;
+ jingle.find('>content').each((_, content) => {
+ const m = this.jingle2media($(content));
+
+ this.media.push(m);
});
+
+ this.raw = this.session + this.media.join('');
}
- return mediaSSRCs;
-};
+ /**
+ * Returns an SSRC Map by extracting SSRCs and SSRC groups from all the m-lines in the SDP.
+ *
+ * @returns {*}
+ */
+ getMediaSsrcMap() {
+ const mediaSSRCs = {};
+
+ this.media.forEach((mediaItem, mediaindex) => {
+ const mid = SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:'));
+ const media = {
+ mediaindex,
+ mid,
+ ssrcs: {},
+ ssrcGroups: []
+ };
+
+ mediaSSRCs[mediaindex] = media;
+
+ SDPUtil.findLines(mediaItem, 'a=ssrc:').forEach(line => {
+ const linessrc = line.substring(7).split(' ')[0];
+
+ // Allocate new ChannelSsrc.
+ if (!media.ssrcs[linessrc]) {
+ media.ssrcs[linessrc] = {
+ ssrc: linessrc,
+ lines: []
+ };
+ }
+ media.ssrcs[linessrc].lines.push(line);
+ });
-/**
- * Returns true if this SDP contains given SSRC.
- * @param ssrc the ssrc to check.
- * @returns {boolean} true if this SDP contains given SSRC.
- */
-SDP.prototype.containsSSRC = function(ssrc) {
- // FIXME this code is really strange - improve it if you can
- const medias = this.getMediaSsrcMap();
- let result = false;
-
- Object.keys(medias).forEach(mediaindex => {
- if (result) {
- return;
+ SDPUtil.findLines(mediaItem, 'a=ssrc-group:').forEach(line => {
+ const idx = line.indexOf(' ');
+ const semantics = line.substr(0, idx).substr(13);
+ const ssrcs = line.substr(14 + semantics.length).split(' ');
+
+ if (ssrcs.length) {
+ media.ssrcGroups.push({
+ semantics,
+ ssrcs
+ });
+ }
+ });
+ });
+
+ return mediaSSRCs;
+ }
+
+ /**
+ * Converts the content section from Jingle to a media section that can be appended to the SDP.
+ *
+ * @param {*} content - The content section from the Jingle message element.
+ * @returns {*} - The constructed media sections.
+ */
+ jingle2media(content) {
+ const desc = content.find('>description');
+ const transport = content.find(`>transport[xmlns='${XEP.ICE_UDP_TRANSPORT}']`);
+ let sdp = '';
+ const sctp = transport.find(`>sctpmap[xmlns='${XEP.SCTP_DATA_CHANNEL}']`);
+ const media = { media: desc.attr('media') };
+
+ media.port = '9';
+ if (content.attr('senders') === 'rejected') {
+ media.port = '0';
}
- if (medias[mediaindex].ssrcs[ssrc]) {
- result = true;
+ if (transport.find(`>fingerprint[xmlns='${XEP.DTLS_SRTP}']`).length) {
+ media.proto = sctp.length ? 'UDP/DTLS/SCTP' : 'UDP/TLS/RTP/SAVPF';
+ } else {
+ media.proto = 'UDP/TLS/RTP/SAVPF';
}
- });
-
- return result;
-};
-
-// add content's to a jingle element
-SDP.prototype.toJingle = function(elem, thecreator) {
- // https://xmpp.org/extensions/xep-0338.html
- SDPUtil.findLines(this.session, 'a=group:').forEach(line => {
- const parts = line.split(' ');
- const semantics = parts.shift().substr(8);
-
- elem.c('group', { xmlns: 'urn:xmpp:jingle:apps:grouping:0',
- semantics });
- for (let j = 0; j < parts.length; j++) {
- elem.c('content', { name: parts[j] }).up();
+ if (sctp.length) {
+ sdp += `m=application ${media.port} UDP/DTLS/SCTP webrtc-datachannel\r\n`;
+ sdp += `a=sctp-port:${sctp.attr('number')}\r\n`;
+ sdp += 'a=max-message-size:262144\r\n';
+ } else {
+ media.fmt
+ = desc
+ .find('>payload-type')
+ .map((_, payloadType) => payloadType.getAttribute('id'))
+ .get();
+ sdp += `${SDPUtil.buildMLine(media)}\r\n`;
}
- elem.up();
- });
- for (let i = 0; i < this.media.length; i++) {
- const mline = SDPUtil.parseMLine(this.media[i].split('\r\n')[0]);
+ sdp += 'c=IN IP4 0.0.0.0\r\n';
+ if (!sctp.length) {
+ sdp += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
+ }
- if (!(mline.media === 'audio'
- || mline.media === 'video'
- || mline.media === 'application')) {
- continue; // eslint-disable-line no-continue
+ if (transport.length) {
+ if (transport.attr('ufrag')) {
+ sdp += `${SDPUtil.buildICEUfrag(transport.attr('ufrag'))}\r\n`;
+ }
+ if (transport.attr('pwd')) {
+ sdp += `${SDPUtil.buildICEPwd(transport.attr('pwd'))}\r\n`;
+ }
+ transport.find(`>fingerprint[xmlns='${XEP.DTLS_SRTP}']`).each((_, fingerprint) => {
+ sdp += `a=fingerprint:${fingerprint.getAttribute('hash')} ${$(fingerprint).text()}\r\n`;
+ if (fingerprint.hasAttribute('setup')) {
+ sdp += `a=setup:${fingerprint.getAttribute('setup')}\r\n`;
+ }
+ });
}
- let ssrc;
- const assrcline = SDPUtil.findLine(this.media[i], 'a=ssrc:');
+ transport.find('>candidate').each((_, candidate) => {
+ let protocol = candidate.getAttribute('protocol');
- if (assrcline) {
- ssrc = assrcline.substring(7).split(' ')[0]; // take the first
- } else {
- ssrc = false;
- }
+ protocol = typeof protocol === 'string' ? protocol.toLowerCase() : '';
- elem.c('content', { creator: thecreator,
- name: mline.media });
- const amidline = SDPUtil.findLine(this.media[i], 'a=mid:');
+ if ((this.removeTcpCandidates && (protocol === 'tcp' || protocol === 'ssltcp'))
+ || (this.removeUdpCandidates && protocol === 'udp')) {
+ return;
+ } else if (this.failICE) {
+ candidate.setAttribute('ip', '1.1.1.1');
+ }
- if (amidline) {
- // prefer identifier from a=mid if present
- const mid = SDPUtil.parseMID(amidline);
+ sdp += SDPUtil.candidateFromJingle(candidate);
+ });
- elem.attrs({ name: mid });
+ switch (content.attr('senders')) {
+ case 'initiator':
+ sdp += `a=${MediaDirection.SENDONLY}\r\n`;
+ break;
+ case 'responder':
+ sdp += `a=${MediaDirection.RECVONLY}\r\n`;
+ break;
+ case 'none':
+ sdp += `a=${MediaDirection.INACTIVE}\r\n`;
+ break;
+ case 'both':
+ sdp += `a=${MediaDirection.SENDRECV}\r\n`;
+ break;
}
+ sdp += `a=mid:${content.attr('name')}\r\n`;
- if (mline.media === 'video' && typeof this.initialLastN === 'number') {
- elem.c('initial-last-n',
- { xmlns: 'jitsi:colibri2',
- value: this.initialLastN }).up();
+ //
+ // see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
+ // and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
+ if (desc.find('>rtcp-mux').length) {
+ sdp += 'a=rtcp-mux\r\n';
}
- if (mline.media === 'audio' || mline.media === 'video') {
- elem.c('description',
- { xmlns: 'urn:xmpp:jingle:apps:rtp:1',
- media: mline.media });
- if (ssrc) {
- elem.attrs({ ssrc });
+ desc.find('>payload-type').each((_, payloadType) => {
+ sdp += `${SDPUtil.buildRTPMap(payloadType)}\r\n`;
+ if ($(payloadType).find('>parameter').length) {
+ sdp += `a=fmtp:${payloadType.getAttribute('id')} `;
+ sdp += $(payloadType)
+ .find('>parameter')
+ .map((__, parameter) => {
+ const name = parameter.getAttribute('name');
+
+ return (name ? `${name}=` : '') + parameter.getAttribute('value');
+ })
+ .get()
+ .join(';');
+ sdp += '\r\n';
}
- for (let j = 0; j < mline.fmt.length; j++) {
- const rtpmap
- = SDPUtil.findLine(
- this.media[i],
- `a=rtpmap:${mline.fmt[j]}`);
-
- elem.c('payload-type', SDPUtil.parseRTPMap(rtpmap));
-
- // put any 'a=fmtp:' + mline.fmt[j] lines into
- const afmtpline
- = SDPUtil.findLine(
- this.media[i],
- `a=fmtp:${mline.fmt[j]}`);
-
- if (afmtpline) {
- const fmtpParameters = SDPUtil.parseFmtp(afmtpline);
-
- // eslint-disable-next-line max-depth
- for (let k = 0; k < fmtpParameters.length; k++) {
- elem.c('parameter', fmtpParameters[k]).up();
- }
+
+ sdp += this.rtcpFbFromJingle($(payloadType), payloadType.getAttribute('id'));
+ });
+
+ sdp += this.rtcpFbFromJingle(desc, '*');
+
+ desc.find(`>rtp-hdrext[xmlns='${XEP.RTP_HEADER_EXTENSIONS}']`).each((_, hdrExt) => {
+ sdp += `a=extmap:${hdrExt.getAttribute('id')} ${hdrExt.getAttribute('uri')}\r\n`;
+ });
+ if (desc.find(`>extmap-allow-mixed[xmlns='${XEP.RTP_HEADER_EXTENSIONS}']`).length > 0) {
+ sdp += 'a=extmap-allow-mixed\r\n';
+ }
+
+ desc
+ .find(`>ssrc-group[xmlns='${XEP.SOURCE_ATTRIBUTES}']`)
+ .each((_, ssrcGroup) => {
+ const semantics = ssrcGroup.getAttribute('semantics');
+ const ssrcs
+ = $(ssrcGroup)
+ .find('>source')
+ .map((__, source) => source.getAttribute('ssrc'))
+ .get();
+
+ if (ssrcs.length) {
+ sdp += `a=ssrc-group:${semantics} ${ssrcs.join(' ')}\r\n`;
}
+ });
- // XEP-0293 -- map a=rtcp-fb
- this.rtcpFbToJingle(i, elem, mline.fmt[j]);
+ let userSources = '';
+ let nonUserSources = '';
- elem.up();
- }
+ desc
+ .find(`>source[xmlns='${XEP.SOURCE_ATTRIBUTES}']`)
+ .each((_, source) => {
+ const ssrc = source.getAttribute('ssrc');
+ let isUserSource = true;
+ let sourceStr = '';
- if (ssrc) {
- const ssrcMap = SDPUtil.parseSSRC(this.media[i]);
+ $(source)
+ .find('>parameter')
+ .each((__, parameter) => {
+ const name = parameter.getAttribute('name');
+ let value = parameter.getAttribute('value');
- for (const [ availableSsrc, ssrcParameters ] of ssrcMap) {
- const sourceName = SDPUtil.parseSourceNameLine(ssrcParameters);
- const videoType = SDPUtil.parseVideoTypeLine(ssrcParameters);
+ value = SDPUtil.filterSpecialChars(value);
+ sourceStr += `a=ssrc:${ssrc} ${name}`;
- elem.c('source', {
- ssrc: availableSsrc,
- name: sourceName,
- videoType,
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
- });
+ if (value && value.length) {
+ sourceStr += `:${value}`;
+ }
- const msid = SDPUtil.parseMSIDAttribute(ssrcParameters);
+ sourceStr += '\r\n';
- // eslint-disable-next-line max-depth
- if (msid) {
- elem.c('parameter');
- elem.attrs({ name: 'msid' });
- elem.attrs({ value: msid });
- elem.up();
- }
+ if (value?.includes('mixedmslabel')) {
+ isUserSource = false;
+ }
+ });
- elem.up();
+ if (isUserSource) {
+ userSources += sourceStr;
+ } else {
+ nonUserSources += sourceStr;
}
+ });
- // XEP-0339 handle ssrc-group attributes
- const ssrcGroupLines
- = SDPUtil.findLines(this.media[i], 'a=ssrc-group:');
+ // Append sources in the correct order, the mixedmslable m-line which has the JVB's SSRC for RTCP termination
+ // is expected to be in the first m-line.
+ sdp += nonUserSources + userSources;
- ssrcGroupLines.forEach(line => {
- const idx = line.indexOf(' ');
- const semantics = line.substr(0, idx).substr(13);
- const ssrcs = line.substr(14 + semantics.length).split(' ');
+ return sdp;
+ }
- if (ssrcs.length) {
- elem.c('ssrc-group', { semantics,
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
- ssrcs.forEach(s => elem.c('source', { ssrc: s }).up());
- elem.up();
- }
- });
+ /**
+ * Coverts the RTCP attributes for the session from XMPP format to SDP.
+ * https://xmpp.org/extensions/xep-0293.html
+ *
+ * @param {*} elem - Jingle message element.
+ * @param {*} payloadtype - Payload type for the codec.
+ * @returns {string}
+ */
+ rtcpFbFromJingle(elem, payloadtype) {
+ let sdp = '';
+ const feedbackElementTrrInt = elem.find(`>rtcp-fb-trr-int[xmlns='${XEP.RTP_FEEDBACK}']`);
+
+ if (feedbackElementTrrInt.length) {
+ sdp += 'a=rtcp-fb:* trr-int ';
+ sdp += feedbackElementTrrInt.attr('value') || '0';
+ sdp += '\r\n';
+ }
+
+ const feedbackElements = elem.find(`>rtcp-fb[xmlns='${XEP.RTP_FEEDBACK}']`);
+
+ feedbackElements.each((_, fb) => {
+ sdp += `a=rtcp-fb:${payloadtype} ${fb.getAttribute('type')}`;
+ if (fb.hasAttribute('subtype')) {
+ sdp += ` ${fb.getAttribute('subtype')}`;
}
+ sdp += '\r\n';
+ });
- const ridLines = SDPUtil.findLines(this.media[i], 'a=rid:');
+ return sdp;
+ }
- if (ridLines.length && browser.usesRidsForSimulcast()) {
- // Map a line which looks like "a=rid:2 send" to just
- // the rid ("2")
- const rids = ridLines
- .map(ridLine => ridLine.split(':')[1])
- .map(ridInfo => ridInfo.split(' ')[0]);
+ /**
+ * Converts the RTCP attributes for the session from SDP to XMPP format.
+ * https://xmpp.org/extensions/xep-0293.html
+ *
+ * @param {*} mediaIndex - The index of the media section in the SDP.
+ * @param {*} elem - The Jingle message element.
+ * @param {*} payloadtype - payload type for the codec.
+ */
+ rtcpFbToJingle(mediaIndex, elem, payloadtype) {
+ const lines = SDPUtil.findLines(this.media[mediaIndex], `a=rtcp-fb:${payloadtype}`);
- rids.forEach(rid => {
- elem.c('source', {
- rid,
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
- });
- elem.up();
- });
- const unifiedSimulcast
- = SDPUtil.findLine(this.media[i], 'a=simulcast:');
+ lines.forEach(line => {
+ const feedback = SDPUtil.parseRTCPFB(line);
- if (unifiedSimulcast) {
- elem.c('rid-group', {
- semantics: 'SIM',
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
- });
- rids.forEach(rid => {
- elem.c('source', { rid }).up();
- });
- elem.up();
+ if (feedback.type === 'trr-int') {
+ elem.c('rtcp-fb-trr-int', {
+ xmlns: XEP.RTP_FEEDBACK,
+ value: feedback.params[0]
+ });
+ elem.up();
+ } else {
+ elem.c('rtcp-fb', {
+ xmlns: XEP.RTP_FEEDBACK,
+ type: feedback.type
+ });
+ if (feedback.params.length > 0) {
+ elem.attrs({ 'subtype': feedback.params[0] });
}
+ elem.up();
}
+ });
+ }
- if (SDPUtil.findLine(this.media[i], 'a=rtcp-mux')) {
- elem.c('rtcp-mux').up();
- }
+ /**
+ * Converts the current SDP to a Jingle message that can be sent over the wire to a signaling server.
+ *
+ * @param {*} elem - The Jingle message element.
+ * @param {*} thecreator - Sender role, whether it is an 'initiator' or 'responder'.
+ * @returns - The updated Jingle message element.
+ */
+ toJingle(elem, thecreator) {
+ SDPUtil.findLines(this.session, 'a=group:').forEach(line => {
+ const parts = line.split(' ');
+ const semantics = parts.shift().substr(8);
+
+ elem.c('group', {
+ xmlns: XEP.BUNDLE_MEDIA,
+ semantics
+ });
+ parts.forEach(part => elem.c('content', { name: part }).up());
+ elem.up();
+ });
- // XEP-0293 -- map a=rtcp-fb:*
- this.rtcpFbToJingle(i, elem, '*');
+ this.media.forEach((mediaItem, i) => {
+ const mline = SDPUtil.parseMLine(mediaItem.split('\r\n')[0]);
- // XEP-0294
- const extmapLines = SDPUtil.findLines(this.media[i], 'a=extmap:', this.session);
+ if (![ MediaType.AUDIO, MediaType.VIDEO, MediaType.APPLICATION ].includes(mline.media)) {
+ return;
+ }
- for (let j = 0; j < extmapLines.length; j++) {
- const extmap = SDPUtil.parseExtmap(extmapLines[j]);
+ let ssrc = false;
+ const assrcline = SDPUtil.findLine(mediaItem, 'a=ssrc:');
- elem.c('rtp-hdrext', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
- uri: extmap.uri,
- id: extmap.value
- });
+ if (assrcline) {
+ ssrc = assrcline.substring(7).split(' ')[0];
+ }
- // eslint-disable-next-line max-depth
- if (extmap.hasOwnProperty('direction')) {
-
- // eslint-disable-next-line max-depth
- switch (extmap.direction) {
- case MediaDirection.SENDONLY:
- elem.attrs({ senders: 'responder' });
- break;
- case MediaDirection.RECVONLY:
- elem.attrs({ senders: 'initiator' });
- break;
- case MediaDirection.SENDRECV:
- elem.attrs({ senders: 'both' });
- break;
- case MediaDirection.INACTIVE:
- elem.attrs({ senders: 'none' });
- break;
- }
- }
+ elem.c('content', {
+ creator: thecreator,
+ name: mline.media
+ });
+ const amidline = SDPUtil.findLine(mediaItem, 'a=mid:');
- // TODO: handle params
- elem.up();
+ if (amidline) {
+ // Prefer identifier from a=mid if present.
+ elem.attrs({ name: SDPUtil.parseMID(amidline) });
}
- if (SDPUtil.findLine(this.media[i], 'a=extmap-allow-mixed', this.session)) {
- elem.c('extmap-allow-mixed', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'
- });
- elem.up();
+ if (mline.media === MediaType.VIDEO && typeof this.initialLastN === 'number') {
+ elem.c('initial-last-n', {
+ xmlns: 'jitsi:colibri2',
+ value: this.initialLastN
+ }).up();
}
- elem.up(); // end of description
- }
- // map ice-ufrag/pwd, dtls fingerprint, candidates
- this.transportToJingle(i, elem);
-
- const m = this.media[i];
+ if ([ MediaType.AUDIO, MediaType.VIDEO ].includes(mline.media)) {
+ elem.c('description', {
+ xmlns: XEP.RTP_MEDIA,
+ media: mline.media
+ });
+ if (ssrc) {
+ elem.attrs({ ssrc });
+ }
- if (SDPUtil.findLine(m, `a=${MediaDirection.SENDRECV}`)) {
- elem.attrs({ senders: 'both' });
- } else if (SDPUtil.findLine(m, `a=${MediaDirection.SENDONLY}`)) {
- elem.attrs({ senders: 'initiator' });
- } else if (SDPUtil.findLine(m, `a=${MediaDirection.RECVONLY}`)) {
- elem.attrs({ senders: 'responder' });
- } else if (SDPUtil.findLine(m, `a=${MediaDirection.INACTIVE}`)) {
- elem.attrs({ senders: 'none' });
- }
+ mline.fmt.forEach(format => {
+ const rtpmap = SDPUtil.findLine(mediaItem, `a=rtpmap:${format}`);
- // Reject an m-line only when port is 0 and a=bundle-only is not present in the section.
- // The port is automatically set to 0 when bundle-only is used.
- if (mline.port === '0' && !SDPUtil.findLine(m, 'a=bundle-only', this.session)) {
- // estos hack to reject an m-line
- elem.attrs({ senders: 'rejected' });
- }
- elem.up(); // end of content
- }
- elem.up();
+ elem.c('payload-type', SDPUtil.parseRTPMap(rtpmap));
- return elem;
-};
+ const afmtpline = SDPUtil.findLine(mediaItem, `a=fmtp:${format}`);
-SDP.prototype.transportToJingle = function(mediaindex, elem) {
- elem.c('transport');
+ if (afmtpline) {
+ const fmtpParameters = SDPUtil.parseFmtp(afmtpline);
- // XEP-0343 DTLS/SCTP
- const sctpport
- = SDPUtil.findLine(this.media[mediaindex], 'a=sctp-port:', this.session);
- const sctpmap
- = SDPUtil.findLine(this.media[mediaindex], 'a=sctpmap:', this.session);
+ fmtpParameters.forEach(param => elem.c('parameter', param).up());
+ }
- if (sctpport) {
- const sctpAttrs = SDPUtil.parseSCTPPort(sctpport);
+ this.rtcpFbToJingle(i, elem, format);
+ elem.up();
+ });
- elem.c('sctpmap', {
- xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
- number: sctpAttrs, /* SCTP port */
- protocol: 'webrtc-datachannel' /* protocol */
- });
+ if (ssrc) {
+ const ssrcMap = SDPUtil.parseSSRC(mediaItem);
- // The parser currently requires streams to be present
- elem.attrs({ streams: 0 });
- elem.up();
- } else if (sctpmap) {
- const sctpAttrs = SDPUtil.parseSCTPMap(sctpmap);
+ for (const [ availableSsrc, ssrcParameters ] of ssrcMap) {
+ const sourceName = SDPUtil.parseSourceNameLine(ssrcParameters);
+ const videoType = SDPUtil.parseVideoTypeLine(ssrcParameters);
- elem.c('sctpmap', {
- xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
- number: sctpAttrs[0], /* SCTP port */
- protocol: sctpAttrs[1] /* protocol */
- });
+ elem.c('source', {
+ ssrc: availableSsrc,
+ name: sourceName,
+ videoType,
+ xmlns: XEP.SOURCE_ATTRIBUTES
+ });
- // Optional stream count attribute
- if (sctpAttrs.length > 2) {
- elem.attrs({ streams: sctpAttrs[2] });
- } else {
- elem.attrs({ streams: 0 });
- }
- elem.up();
- }
+ const msid = SDPUtil.parseMSIDAttribute(ssrcParameters);
- // XEP-0320
- const fingerprints
- = SDPUtil.findLines(
- this.media[mediaindex],
- 'a=fingerprint:',
- this.session);
+ if (msid) {
+ elem.c('parameter').attrs({
+ name: 'msid',
+ value: msid
+ });
+ elem.up();
+ }
- fingerprints.forEach(line => {
- const fingerprint = SDPUtil.parseFingerprint(line);
+ elem.up();
+ }
- fingerprint.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
- elem.c('fingerprint').t(fingerprint.fingerprint);
- delete fingerprint.fingerprint;
+ const ssrcGroupLines = SDPUtil.findLines(mediaItem, 'a=ssrc-group:');
+
+ ssrcGroupLines.forEach(line => {
+ const idx = line.indexOf(' ');
+ const semantics = line.substr(0, idx).substr(13);
+ const ssrcs = line.substr(14 + semantics.length).split(' ');
+
+ if (ssrcs.length) {
+ elem.c('ssrc-group', {
+ semantics,
+ xmlns: XEP.SOURCE_ATTRIBUTES
+ });
+ ssrcs.forEach(s => elem.c('source', { ssrc: s }).up());
+ elem.up();
+ }
+ });
+ }
- const setupLine
- = SDPUtil.findLine(
- this.media[mediaindex],
- 'a=setup:',
- this.session);
+ const ridLines = SDPUtil.findLines(mediaItem, 'a=rid:');
- if (setupLine) {
- fingerprint.setup = setupLine.substr(8);
- }
- elem.attrs(fingerprint);
- elem.up(); // end of fingerprint
- });
- const iceParameters = SDPUtil.iceparams(this.media[mediaindex], this.session);
-
- if (iceParameters) {
- iceParameters.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
- elem.attrs(iceParameters);
-
- // XEP-0176
- const candidateLines
- = SDPUtil.findLines(
- this.media[mediaindex],
- 'a=candidate:',
- this.session);
-
- candidateLines.forEach(line => { // add any a=candidate lines
- const candidate = SDPUtil.candidateToJingle(line);
-
- if (this.failICE) {
- candidate.ip = '1.1.1.1';
- }
- const protocol
- = candidate && typeof candidate.protocol === 'string'
- ? candidate.protocol.toLowerCase()
- : '';
+ if (ridLines.length && browser.usesRidsForSimulcast()) {
+ // Map a line which looks like "a=rid:2 send" to just the rid ("2").
+ const rids = ridLines.map(ridLine => ridLine.split(':')[1].split(' ')[0]);
- if ((this.removeTcpCandidates
- && (protocol === 'tcp' || protocol === 'ssltcp'))
- || (this.removeUdpCandidates && protocol === 'udp')) {
- return;
- }
- elem.c('candidate', candidate).up();
- });
- }
- elem.up(); // end of transport
-};
-
-// XEP-0293
-SDP.prototype.rtcpFbToJingle = function(mediaindex, elem, payloadtype) {
- const lines
- = SDPUtil.findLines(
- this.media[mediaindex],
- `a=rtcp-fb:${payloadtype}`);
-
- lines.forEach(line => {
- const feedback = SDPUtil.parseRTCPFB(line);
-
- if (feedback.type === 'trr-int') {
- elem.c('rtcp-fb-trr-int', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0',
- value: feedback.params[0]
- });
- elem.up();
- } else {
- elem.c('rtcp-fb', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0',
- type: feedback.type
- });
- if (feedback.params.length > 0) {
- elem.attrs({ 'subtype': feedback.params[0] });
- }
- elem.up();
- }
- });
-};
-
-SDP.prototype.rtcpFbFromJingle = function(elem, payloadtype) { // XEP-0293
- let sdp = '';
- const feedbackElementTrrInt
- = elem.find(
- '>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
-
- if (feedbackElementTrrInt.length) {
- sdp += 'a=rtcp-fb:* trr-int ';
- if (feedbackElementTrrInt.attr('value')) {
- sdp += feedbackElementTrrInt.attr('value');
- } else {
- sdp += '0';
- }
- sdp += '\r\n';
- }
+ rids.forEach(rid => {
+ elem.c('source', {
+ rid,
+ xmlns: XEP.SOURCE_ATTRIBUTES
+ });
+ elem.up();
+ });
- const feedbackElements = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
+ const unifiedSimulcast = SDPUtil.findLine(mediaItem, 'a=simulcast:');
- feedbackElements.each((_, fb) => {
- sdp += `a=rtcp-fb:${payloadtype} ${fb.getAttribute('type')}`;
- if (fb.hasAttribute('subtype')) {
- sdp += ` ${fb.getAttribute('subtype')}`;
- }
- sdp += '\r\n';
- });
-
- return sdp;
-};
-
-// construct an SDP from a jingle stanza
-SDP.prototype.fromJingle = function(jingle) {
- const sessionId = Date.now();
-
- // Use a unique session id for every TPC.
- this.raw = 'v=0\r\n'
- + `o=- ${sessionId} 2 IN IP4 0.0.0.0\r\n`
- + 's=-\r\n'
- + 't=0 0\r\n';
-
- // http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04
- // #section-8
- const groups
- = $(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]');
-
- if (groups.length) {
- groups.each((idx, group) => {
- const contents
- = $(group)
- .find('>content')
- .map((_, content) => content.getAttribute('name'))
- .get();
+ if (unifiedSimulcast) {
+ elem.c('rid-group', {
+ semantics: 'SIM',
+ xmlns: XEP.SOURCE_ATTRIBUTES
+ });
+ rids.forEach(rid => elem.c('source', { rid }).up());
+ elem.up();
+ }
+ }
- if (contents.length > 0) {
- this.raw
- += `a=group:${
- group.getAttribute('semantics')
- || group.getAttribute('type')} ${
- contents.join(' ')}\r\n`;
- }
- });
- }
+ if (SDPUtil.findLine(mediaItem, 'a=rtcp-mux')) {
+ elem.c('rtcp-mux').up();
+ }
- this.session = this.raw;
- jingle.find('>content').each((_, content) => {
- const m = this.jingle2media($(content));
+ this.rtcpFbToJingle(i, elem, '*');
- this.media.push(m);
- });
+ const extmapLines = SDPUtil.findLines(mediaItem, 'a=extmap:', this.session);
- // reconstruct msid-semantic -- apparently not necessary
- /*
- var msid = SDPUtil.parseSSRC(this.raw);
- if (msid.hasOwnProperty('mslabel')) {
- this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
- }
- */
+ extmapLines.forEach(extmapLine => {
+ const extmap = SDPUtil.parseExtmap(extmapLine);
- this.raw = this.session + this.media.join('');
-};
+ elem.c('rtp-hdrext', {
+ xmlns: XEP.RTP_HEADER_EXTENSIONS,
+ uri: extmap.uri,
+ id: extmap.value
+ });
-// translate a jingle content element into an an SDP media part
-SDP.prototype.jingle2media = function(content) {
- const desc = content.find('>description');
- const transport = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
- let sdp = '';
- const sctp = transport.find(
- '>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
+ if (extmap.hasOwnProperty('direction')) {
+ switch (extmap.direction) {
+ case MediaDirection.SENDONLY:
+ elem.attrs({ senders: 'responder' });
+ break;
+ case MediaDirection.RECVONLY:
+ elem.attrs({ senders: 'initiator' });
+ break;
+ case MediaDirection.SENDRECV:
+ elem.attrs({ senders: 'both' });
+ break;
+ case MediaDirection.INACTIVE:
+ elem.attrs({ senders: 'none' });
+ break;
+ }
+ }
- const media = { media: desc.attr('media') };
+ elem.up();
+ });
- media.port = '9';
- if (content.attr('senders') === 'rejected') {
- // estos hack to reject an m-line.
- media.port = '0';
- }
- if (transport.find('>fingerprint[xmlns="urn:xmpp:jingle:apps:dtls:0"]').length) {
- media.proto = sctp.length ? 'UDP/DTLS/SCTP' : 'UDP/TLS/RTP/SAVPF';
- } else {
- media.proto = 'UDP/TLS/RTP/SAVPF';
- }
- if (sctp.length) {
- sdp += `m=application ${media.port} UDP/DTLS/SCTP webrtc-datachannel\r\n`;
- sdp += `a=sctp-port:${sctp.attr('number')}\r\n`;
- sdp += 'a=max-message-size:262144\r\n';
- } else {
- media.fmt
- = desc
- .find('>payload-type')
- .map((_, payloadType) => payloadType.getAttribute('id'))
- .get();
- sdp += `${SDPUtil.buildMLine(media)}\r\n`;
- }
+ if (SDPUtil.findLine(mediaItem, 'a=extmap-allow-mixed', this.session)) {
+ elem.c('extmap-allow-mixed', {
+ xmlns: XEP.RTP_HEADER_EXTENSIONS
+ });
+ elem.up();
+ }
+ elem.up(); // end of description
+ }
- sdp += 'c=IN IP4 0.0.0.0\r\n';
- if (!sctp.length) {
- sdp += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
- }
+ // Map ice-ufrag/pwd, dtls fingerprint, candidates.
+ this.transportToJingle(i, elem);
+
+ // Set senders attribute based on media direction
+ if (SDPUtil.findLine(mediaItem, `a=${MediaDirection.SENDRECV}`)) {
+ elem.attrs({ senders: 'both' });
+ } else if (SDPUtil.findLine(mediaItem, `a=${MediaDirection.SENDONLY}`)) {
+ elem.attrs({ senders: 'initiator' });
+ } else if (SDPUtil.findLine(mediaItem, `a=${MediaDirection.RECVONLY}`)) {
+ elem.attrs({ senders: 'responder' });
+ } else if (SDPUtil.findLine(mediaItem, `a=${MediaDirection.INACTIVE}`)) {
+ elem.attrs({ senders: 'none' });
+ }
- // XEP-0176 ICE parameters
- if (transport.length) {
- if (transport.attr('ufrag')) {
- sdp += `${SDPUtil.buildICEUfrag(transport.attr('ufrag'))}\r\n`;
- }
- if (transport.attr('pwd')) {
- sdp += `${SDPUtil.buildICEPwd(transport.attr('pwd'))}\r\n`;
- }
- transport.find('>fingerprint[xmlns="urn:xmpp:jingle:apps:dtls:0"]').each((_, fingerprint) => {
- sdp += `a=fingerprint:${fingerprint.getAttribute('hash')}`;
- sdp += ` ${$(fingerprint).text()}`;
- sdp += '\r\n';
- if (fingerprint.hasAttribute('setup')) {
- sdp += `a=setup:${fingerprint.getAttribute('setup')}\r\n`;
+ // Reject an m-line only when port is 0 and a=bundle-only is not present in the section.
+ // The port is automatically set to 0 when bundle-only is used.
+ if (mline.port === '0' && !SDPUtil.findLine(mediaItem, 'a=bundle-only', this.session)) {
+ elem.attrs({ senders: 'rejected' });
}
+ elem.up(); // end of content
});
+ elem.up();
+
+ return elem;
}
- // XEP-0176 ICE candidates
- transport.find('>candidate')
- .each((_, candidate) => {
- let protocol = candidate.getAttribute('protocol');
+ /**
+ * Converts the session transport information from SDP to XMPP format.
+ *
+ * @param {*} mediaIndex The index for the m-line in the SDP.
+ * @param {*} elem The transport element.
+ */
+ transportToJingle(mediaIndex, elem) {
+ elem.c('transport');
- protocol
- = typeof protocol === 'string' ? protocol.toLowerCase() : '';
+ const sctpport = SDPUtil.findLine(this.media[mediaIndex], 'a=sctp-port:', this.session);
+ const sctpmap = SDPUtil.findLine(this.media[mediaIndex], 'a=sctpmap:', this.session);
- if ((this.removeTcpCandidates
- && (protocol === 'tcp' || protocol === 'ssltcp'))
- || (this.removeUdpCandidates && protocol === 'udp')) {
- return;
- } else if (this.failICE) {
- candidate.setAttribute('ip', '1.1.1.1');
- }
+ if (sctpport) {
+ const sctpAttrs = SDPUtil.parseSCTPPort(sctpport);
- sdp += SDPUtil.candidateFromJingle(candidate);
- });
+ elem.c('sctpmap', {
+ xmlns: XEP.SCTP_DATA_CHANNEL,
+ number: sctpAttrs, // SCTP port
+ protocol: 'webrtc-datachannel' // protocol
+ });
- switch (content.attr('senders')) {
- case 'initiator':
- sdp += `a=${MediaDirection.SENDONLY}\r\n`;
- break;
- case 'responder':
- sdp += `a=${MediaDirection.RECVONLY}\r\n`;
- break;
- case 'none':
- sdp += `a=${MediaDirection.INACTIVE}\r\n`;
- break;
- case 'both':
- sdp += `a=${MediaDirection.SENDRECV}\r\n`;
- break;
- }
- sdp += `a=mid:${content.attr('name')}\r\n`;
-
- //
- // see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec
- // though
- // and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
- if (desc.find('>rtcp-mux').length) {
- sdp += 'a=rtcp-mux\r\n';
- }
+ // The parser currently requires streams to be present.
+ elem.attrs({ streams: 0 });
+ elem.up();
+ } else if (sctpmap) {
+ const sctpAttrs = SDPUtil.parseSCTPMap(sctpmap);
- desc.find('>payload-type').each((_, payloadType) => {
- sdp += `${SDPUtil.buildRTPMap(payloadType)}\r\n`;
- if ($(payloadType).find('>parameter').length) {
- sdp += `a=fmtp:${payloadType.getAttribute('id')} `;
- sdp
- += $(payloadType)
- .find('>parameter')
- .map((__, parameter) => {
- const name = parameter.getAttribute('name');
+ elem.c('sctpmap', {
+ xmlns: XEP.SCTP_DATA_CHANNEL,
+ number: sctpAttrs[0], // SCTP port
+ protocol: sctpAttrs[1] // protocol
+ });
- return (
- (name ? `${name}=` : '')
- + parameter.getAttribute('value'));
- })
- .get()
- .join(';');
- sdp += '\r\n';
+ // Optional stream count attribute.
+ elem.attrs({ streams: sctpAttrs.length > 2 ? sctpAttrs[2] : 0 });
+ elem.up();
}
- // xep-0293
- sdp += this.rtcpFbFromJingle($(payloadType), payloadType.getAttribute('id'));
- });
+ const fingerprints = SDPUtil.findLines(this.media[mediaIndex], 'a=fingerprint:', this.session);
- // xep-0293
- sdp += this.rtcpFbFromJingle(desc, '*');
+ fingerprints.forEach(line => {
+ const fingerprint = SDPUtil.parseFingerprint(line);
- // xep-0294
- desc
- .find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]')
- .each((_, hdrExt) => {
- sdp
- += `a=extmap:${hdrExt.getAttribute('id')} ${
- hdrExt.getAttribute('uri')}\r\n`;
- });
- if (desc.find('>extmap-allow-mixed[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]').length > 0) {
- sdp += 'a=extmap-allow-mixed\r\n';
- }
+ fingerprint.xmlns = XEP.DTLS_SRTP;
- // XEP-0339 handle ssrc-group attributes
- desc
- .find('>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]')
- .each((_, ssrcGroup) => {
- const semantics = ssrcGroup.getAttribute('semantics');
- const ssrcs
- = $(ssrcGroup)
- .find('>source')
- .map((__, source) => source.getAttribute('ssrc'))
- .get();
+ elem.c('fingerprint').t(fingerprint.fingerprint);
+ delete fingerprint.fingerprint;
+
+ const setupLine = SDPUtil.findLine(this.media[mediaIndex], 'a=setup:', this.session);
- if (ssrcs.length) {
- sdp += `a=ssrc-group:${semantics} ${ssrcs.join(' ')}\r\n`;
+ if (setupLine) {
+ fingerprint.setup = setupLine.substr(8);
}
+ elem.attrs(fingerprint);
+ elem.up(); // end of fingerprint
});
- // XEP-0339 handle source attributes
- let userSources = '';
- let nonUserSources = '';
+ const iceParameters = SDPUtil.iceparams(this.media[mediaIndex], this.session);
- desc
- .find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]')
- .each((_, source) => {
- const ssrc = source.getAttribute('ssrc');
- let isUserSource = true;
- let sourceStr = '';
+ if (iceParameters) {
+ iceParameters.xmlns = XEP.ICE_UDP_TRANSPORT;
+ elem.attrs(iceParameters);
- $(source)
- .find('>parameter')
- .each((__, parameter) => {
- const name = parameter.getAttribute('name');
- let value = parameter.getAttribute('value');
+ const candidateLines = SDPUtil.findLines(this.media[mediaIndex], 'a=candidate:', this.session);
- value = SDPUtil.filterSpecialChars(value);
- sourceStr += `a=ssrc:${ssrc} ${name}`;
+ candidateLines.forEach(line => { // add any a=candidate lines
+ const candidate = SDPUtil.candidateToJingle(line);
- if (value && value.length) {
- sourceStr += `:${value}`;
- }
-
- sourceStr += '\r\n';
-
- if (value?.includes('mixedmslabel')) {
- isUserSource = false;
- }
- });
-
- if (isUserSource) {
- userSources += sourceStr;
- } else {
- nonUserSources += sourceStr;
- }
- });
+ if (this.failICE) {
+ candidate.ip = '1.1.1.1';
+ }
- // The sdp-interop package is relying the mixedmslabel m line to be the first one in order to set the direction
- // to sendrecv.
- sdp += nonUserSources + userSources;
+ const protocol = candidate && typeof candidate.protocol === 'string'
+ ? candidate.protocol.toLowerCase() : '';
- return sdp;
-};
+ if ((this.removeTcpCandidates && (protocol === 'tcp' || protocol === 'ssltcp'))
+ || (this.removeUdpCandidates && protocol === 'udp')) {
+ return;
+ }
+ elem.c('candidate', candidate).up();
+ });
+ }
+ elem.up(); // end of transport
+ }
+}
diff --git a/modules/sdp/SDPDiffer.js b/modules/sdp/SDPDiffer.js
index 9d01f991c7..0abec4597b 100644
--- a/modules/sdp/SDPDiffer.js
+++ b/modules/sdp/SDPDiffer.js
@@ -1,4 +1,6 @@
+import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
+
import SDPUtil from './SDPUtil';
// this could be useful in Array.prototype.
@@ -159,9 +161,10 @@ SDPDiffer.prototype.toJingle = function(modify) {
modify.c('content', { name: media.mid });
- modify.c('description',
- { xmlns: 'urn:xmpp:jingle:apps:rtp:1',
- media: media.mid });
+ modify.c('description', {
+ xmlns: XEP.RTP_MEDIA,
+ media: media.mid
+ });
// FIXME: not completely sure this operates on blocks and / or handles
// different ssrcs correctly
@@ -172,7 +175,7 @@ SDPDiffer.prototype.toJingle = function(modify) {
const sourceName = SDPUtil.parseSourceNameLine(ssrcLines);
const videoType = SDPUtil.parseVideoTypeLine(ssrcLines);
- modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ modify.c('source', { xmlns: XEP.SOURCE_ATTRIBUTES });
modify.attrs({
name: sourceName,
videoType,
@@ -198,7 +201,7 @@ SDPDiffer.prototype.toJingle = function(modify) {
modify.c('ssrc-group', {
semantics: ssrcGroup.semantics,
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
+ xmlns: XEP.SOURCE_ATTRIBUTES
});
ssrcGroup.ssrcs.forEach(ssrc => {
diff --git a/modules/xmpp/JingleHelperFunctions.js b/modules/xmpp/JingleHelperFunctions.js
index c790675f25..ef0790385e 100644
--- a/modules/xmpp/JingleHelperFunctions.js
+++ b/modules/xmpp/JingleHelperFunctions.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import { $build } from 'strophe.js';
import { MediaType } from '../../service/RTC/MediaType';
+import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
const logger = getLogger(__filename);
@@ -15,7 +16,7 @@ const logger = getLogger(__filename);
*/
function _createSourceExtension(owner, sourceCompactJson) {
const node = $build('source', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
+ xmlns: XEP.SOURCE_ATTRIBUTES,
ssrc: sourceCompactJson.s,
name: sourceCompactJson.n
});
@@ -41,13 +42,13 @@ function _createSourceExtension(owner, sourceCompactJson) {
*/
function _createSsrcGroupExtension(ssrcGroupCompactJson) {
const node = $build('ssrc-group', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
+ xmlns: XEP.SOURCE_ATTRIBUTES,
semantics: _getSemantics(ssrcGroupCompactJson[0])
});
for (let i = 1; i < ssrcGroupCompactJson.length; i++) {
node.c('source', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
+ xmlns: XEP.SOURCE_ATTRIBUTES,
ssrc: ssrcGroupCompactJson[i]
}).up();
}
@@ -83,7 +84,7 @@ function _getOrCreateRtpDescription(iq, mediaType) {
description = description[0];
} else {
description = $build('description', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:1',
+ xmlns: XEP.RTP_MEDIA,
media: mediaType
}).node;
content.appendChild(description);
diff --git a/modules/xmpp/JingleSessionPC.js b/modules/xmpp/JingleSessionPC.js
index 37b7de6fd0..af8411182e 100644
--- a/modules/xmpp/JingleSessionPC.js
+++ b/modules/xmpp/JingleSessionPC.js
@@ -13,6 +13,7 @@ import {
VIDEO_CODEC_CHANGED
} from '../../service/statistics/AnalyticsEvents';
import { XMPPEvents } from '../../service/xmpp/XMPPEvents';
+import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
import { SS_DEFAULT_FRAME_RATE } from '../RTC/ScreenObtainer';
import FeatureFlags from '../flags/FeatureFlags';
import SDP from '../sdp/SDP';
@@ -68,7 +69,7 @@ function getEndpointId(jidOrEndpointId) {
function _addSourceElement(description, s, ssrc_, msid) {
description.c('source', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
+ xmlns: XEP.SOURCE_ATTRIBUTES,
ssrc: ssrc_,
name: s.source
})
@@ -713,7 +714,7 @@ export default class JingleSessionPC extends JingleSession {
return;
}
- ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
+ ice.xmlns = XEP.ICE_UDP_TRANSPORT;
if (this.usedrip) {
if (this.dripContainer.length === 0) {
@@ -768,7 +769,7 @@ export default class JingleSessionPC extends JingleSession {
const ice
= SDPUtil.iceparams(localSDP.media[mid], localSDP.session);
- ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
+ ice.xmlns = XEP.ICE_UDP_TRANSPORT;
cand.c('content', {
creator: this.initiatorJid === this.localJid
? 'initiator' : 'responder',
@@ -1834,7 +1835,7 @@ export default class JingleSessionPC extends JingleSession {
xmlns: 'urn:xmpp:jingle:1',
name: mediaType
}).c('description', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:1',
+ xmlns: XEP.RTP_MEDIA,
media: mediaType
});
@@ -1850,16 +1851,16 @@ export default class JingleSessionPC extends JingleSession {
if (rtx !== '-1') {
_addSourceElement(node, src, rtx, msid);
node.c('ssrc-group', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
+ xmlns: XEP.SOURCE_ATTRIBUTES,
semantics: 'FID'
})
.c('source', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
+ xmlns: XEP.SOURCE_ATTRIBUTES,
ssrc
})
.up()
.c('source', {
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
+ xmlns: XEP.SOURCE_ATTRIBUTES,
ssrc: rtx
})
.up()
diff --git a/modules/xmpp/xmpp.js b/modules/xmpp/xmpp.js
index b0b6b17b88..a84febca6a 100644
--- a/modules/xmpp/xmpp.js
+++ b/modules/xmpp/xmpp.js
@@ -7,6 +7,7 @@ import 'strophejs-plugin-disco';
import * as JitsiConnectionErrors from '../../JitsiConnectionErrors';
import * as JitsiConnectionEvents from '../../JitsiConnectionEvents';
import { XMPPEvents } from '../../service/xmpp/XMPPEvents';
+import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
import browser from '../browser';
import { E2EEncryption } from '../e2ee/E2EEncryption';
import FeatureFlags from '../flags/FeatureFlags';
@@ -218,13 +219,13 @@ export default class XMPP extends Listenable {
initFeaturesList() {
// http://xmpp.org/extensions/xep-0167.html#support
// http://xmpp.org/extensions/xep-0176.html#support
- this.caps.addFeature('urn:xmpp:jingle:1');
- this.caps.addFeature('urn:xmpp:jingle:apps:rtp:1');
- this.caps.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
- this.caps.addFeature('urn:xmpp:jingle:apps:dtls:0');
- this.caps.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1');
- this.caps.addFeature('urn:xmpp:jingle:apps:rtp:audio');
- this.caps.addFeature('urn:xmpp:jingle:apps:rtp:video');
+ this.caps.addFeature(XEP.JINGLE);
+ this.caps.addFeature(XEP.RTP_MEDIA);
+ this.caps.addFeature(XEP.ICE_UDP_TRANSPORT);
+ this.caps.addFeature(XEP.DTLS_SRTP);
+ this.caps.addFeature(XEP.SCTP_DATA_CHANNEL);
+ this.caps.addFeature(XEP.RTP_AUDIO);
+ this.caps.addFeature(XEP.RTP_VIDEO);
this.caps.addFeature('http://jitsi.org/json-encoded-sources');
if (!(this.options.disableRtx || !browser.supportsRTX())) {
diff --git a/service/RTC/MediaType.ts b/service/RTC/MediaType.ts
index eaf8be6781..09509da072 100644
--- a/service/RTC/MediaType.ts
+++ b/service/RTC/MediaType.ts
@@ -7,5 +7,10 @@ export enum MediaType {
/**
* The video type.
*/
- VIDEO = 'video'
+ VIDEO = 'video',
+
+ /**
+ * The application type (data over bridge channel).
+ */
+ APPLICATION = 'application'
}
diff --git a/service/xmpp/XMPPExtensioProtocols.ts b/service/xmpp/XMPPExtensioProtocols.ts
new file mode 100644
index 0000000000..f79017b686
--- /dev/null
+++ b/service/xmpp/XMPPExtensioProtocols.ts
@@ -0,0 +1,74 @@
+
+export enum XEP {
+ /**
+ * XEP-0338 - Signals the usage of bundled media, i.e., allows the use of a single set of ICE candidates for
+ * multiple media descriptions.
+ * https://xmpp.org/extensions/attic/xep-0338-1.0.0.html
+ */
+ BUNDLE_MEDIA = 'urn:xmpp:jingle:apps:grouping:0',
+
+ /**
+ * XEP-0320 - Signals the use of DTLS-SRTP in Jingle session.
+ * https://xmpp.org/extensions/xep-0320.html
+ */
+ DTLS_SRTP = 'urn:xmpp:jingle:apps:dtls:0',
+
+ /**
+ * XEP-0176 - Signaling ICE-UDP transport method.
+ * https://xmpp.org/extensions/xep-0176.html
+ */
+ ICE_UDP_TRANSPORT = 'urn:xmpp:jingle:transports:ice-udp:1',
+
+ /**
+ * XEP-0166 - Jingle.
+ * https://xmpp.org/extensions/xep-0166.html
+ */
+ JINGLE = 'urn:xmpp:jingle:1',
+
+ /**
+ * XEP-0327 - Rayo for allowing third-party control over media sessions.
+ */
+ RAYO = 'urn:xmpp:rayo:client:1',
+
+ /**
+ * XEP-0167 - Signals support for RTP audio.
+ * https://xmpp.org/extensions/xep-0167.html#support
+ */
+ RTP_AUDIO = 'urn:xmpp:jingle:apps:rtp:audio',
+
+ /**
+ * XEP-0293 - Signals the use of RTP Feedback Negotiation.
+ * https://xmpp.org/extensions/xep-0293.html
+ */
+ RTP_FEEDBACK = 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0',
+
+ /**
+ * XEP-0167 - Signals support for RTP video.
+ * https://xmpp.org/extensions/xep-0167.html#support
+ */
+ RTP_VIDEO = 'urn:xmpp:jingle:apps:rtp:video',
+
+ /**
+ * XEP-0294 - Signals the use of RTP Header Extensions.
+ * https://xmpp.org/extensions/xep-0294.html
+ */
+ RTP_HEADER_EXTENSIONS = 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
+
+ /**
+ * XEP-0167 - Signals parameters necessary for media sessions using RTP.
+ * https://xmpp.org/extensions/xep-0167.html
+ */
+ RTP_MEDIA = 'urn:xmpp:jingle:apps:rtp:1',
+
+ /**
+ * XEP-0343 - Signaling WebRTC datachannels (bridge channel) in Jingle that uses DTLS/SCTP on top of ICE.
+ * https://xmpp.org/extensions/xep-0343.html
+ */
+ SCTP_DATA_CHANNEL = 'urn:xmpp:jingle:transports:dtls-sctp:1',
+
+ /**
+ * XEP-0339 - Signals Source-Specific Media Attributes in Jingle.
+ * https://xmpp.org/extensions/xep-0339.html
+ */
+ SOURCE_ATTRIBUTES = 'urn:xmpp:jingle:apps:rtp:ssma:0'
+}