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' +}