/**
* Function that modifies the session description to configure settings for OPUS audio codec.
* @method _setSDPCodecParams
* @private
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype._setSDPCodecParams = function(targetMid, sessionDescription) {
var self = this;
var parseFn = function (type, codecName, samplingRate, settings) {
var mLine = sessionDescription.sdp.match(new RegExp('m=' + type + '\ .*\r\n', 'gi'));
// Find the m= line
if (Array.isArray(mLine) && mLine.length > 0) {
var codecsList = sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' + codecName + '\/' +
(samplingRate ? samplingRate + (type === 'audio' ? '[\/]*.*' : '.*') : '.*') + '\r\n', 'gi'));
// Get the list of codecs related to it
if (Array.isArray(codecsList) && codecsList.length > 0) {
for (var i = 0; i < codecsList.length; i++) {
var payload = (codecsList[i].split('a=rtpmap:')[1] || '').split(' ')[0];
if (!payload) {
continue;
}
var fmtpLine = sessionDescription.sdp.match(new RegExp('a=fmtp:' + payload + '\ .*\r\n', 'gi'));
var updatedFmtpLine = 'a=fmtp:' + payload + ' ';
var addedKeys = [];
// Check if a=fmtp: line exists
if (Array.isArray(fmtpLine) && fmtpLine.length > 0) {
var fmtpParts = (fmtpLine[0].split('a=fmtp:' + payload + ' ')[1] || '').replace(
/ /g, '').replace(/\r\n/g, '').split(';');
for (var j = 0; j < fmtpParts.length; j++) {
if (!fmtpParts[j]) {
continue;
}
var keyAndValue = fmtpParts[j].split('=');
if (settings.hasOwnProperty(keyAndValue[0])) {
// Dont append parameter key+value if boolean and false
updatedFmtpLine += typeof settings[keyAndValue[0]] === 'boolean' ? (settings[keyAndValue[0]] ?
keyAndValue[0] + '=1;' : '') : keyAndValue[0] + '=' + settings[keyAndValue[0]] + ';';
} else {
updatedFmtpLine += fmtpParts[j] + ';';
}
addedKeys.push(keyAndValue[0]);
}
sessionDescription.sdp = sessionDescription.sdp.replace(fmtpLine[0], '');
}
for (var key in settings) {
if (settings.hasOwnProperty(key) && addedKeys.indexOf(key) === -1) {
// Dont append parameter key+value if boolean and false
updatedFmtpLine += typeof settings[key] === 'boolean' ? (settings[key] ? key + '=1;' : '') :
key + '=' + settings[key] + ';';
addedKeys.push(key);
}
}
if (updatedFmtpLine !== 'a=fmtp:' + payload + ' ') {
sessionDescription.sdp = sessionDescription.sdp.replace(codecsList[i], codecsList[i] + updatedFmtpLine + '\r\n');
}
}
}
}
};
// Set audio codecs -> OPUS
// RFC: https://tools.ietf.org/html/draft-ietf-payload-rtp-opus-11
parseFn('audio', self.AUDIO_CODEC.OPUS, 48000, (function () {
var opusOptions = {};
var audioSettings = self._streams.screenshare ? self._streams.screenshare.settings.audio :
(self._streams.userMedia ? self._streams.userMedia.settings.audio : {});
audioSettings = audioSettings && typeof audioSettings === 'object' ? audioSettings : {};
if (typeof self._initOptions.codecParams.audio.opus.stereo === 'boolean') {
opusOptions.stereo = self._initOptions.codecParams.audio.opus.stereo;
} else if (typeof audioSettings.stereo === 'boolean') {
opusOptions.stereo = audioSettings.stereo;
}
if (typeof self._initOptions.codecParams.audio.opus['sprop-stereo'] === 'boolean') {
opusOptions['sprop-stereo'] = self._initOptions.codecParams.audio.opus['sprop-stereo'];
} else if (typeof audioSettings.stereo === 'boolean') {
opusOptions['sprop-stereo'] = audioSettings.stereo;
}
if (typeof self._initOptions.codecParams.audio.opus.usedtx === 'boolean') {
opusOptions.usedtx = self._initOptions.codecParams.audio.opus.usedtx;
} else if (typeof audioSettings.usedtx === 'boolean') {
opusOptions.usedtx = audioSettings.usedtx;
}
if (typeof self._initOptions.codecParams.audio.opus.useinbandfec === 'boolean') {
opusOptions.useinbandfec = self._initOptions.codecParams.audio.opus.useinbandfec;
} else if (typeof audioSettings.useinbandfec === 'boolean') {
opusOptions.useinbandfec = audioSettings.useinbandfec;
}
if (typeof self._initOptions.codecParams.audio.opus.maxplaybackrate === 'number') {
opusOptions.maxplaybackrate = self._initOptions.codecParams.audio.opus.maxplaybackrate;
} else if (typeof audioSettings.maxplaybackrate === 'number') {
opusOptions.maxplaybackrate = audioSettings.maxplaybackrate;
}
if (typeof self._initOptions.codecParams.audio.opus.minptime === 'number') {
opusOptions.minptime = self._initOptions.codecParams.audio.opus.minptime;
} else if (typeof audioSettings.minptime === 'number') {
opusOptions.minptime = audioSettings.minptime;
}
// Possible future params: sprop-maxcapturerate, maxaveragebitrate, sprop-stereo, cbr
// NOT recommended: maxptime, ptime, rate, minptime
return opusOptions;
})());
// RFC: https://tools.ietf.org/html/rfc4733
// Future: Set telephone-event: 100 0-15,66,70
// RFC: https://tools.ietf.org/html/draft-ietf-payload-vp8-17
// Set video codecs -> VP8
parseFn('video', self.VIDEO_CODEC.VP8, null, (function () {
var vp8Options = {};
// NOT recommended: max-fr, max-fs (all are codec decoder capabilities)
if (typeof self._initOptions.codecParams.video.vp8.maxFr === 'number') {
vp8Options['max-fr'] = self._initOptions.codecParams.video.vp8.maxFr;
}
if (typeof self._initOptions.codecParams.video.vp8.maxFs === 'number') {
vp8Options['max-fs'] = self._initOptions.codecParams.video.vp8.maxFs;
}
return vp8Options;
})());
// RFC: https://tools.ietf.org/html/draft-ietf-payload-vp9-02
// Set video codecs -> VP9
parseFn('video', self.VIDEO_CODEC.VP9, null, (function () {
var vp9Options = {};
// NOT recommended: max-fr, max-fs (all are codec decoder capabilities)
if (typeof self._initOptions.codecParams.video.vp9.maxFr === 'number') {
vp9Options['max-fr'] = self._initOptions.codecParams.video.vp9.maxFr;
}
if (typeof self._initOptions.codecParams.video.vp9.maxFs === 'number') {
vp9Options['max-fs'] = self._initOptions.codecParams.video.vp9.maxFs;
}
return vp9Options;
})());
// RFC: https://tools.ietf.org/html/rfc6184
// Set the video codecs -> H264
parseFn('video', self.VIDEO_CODEC.H264, null, (function () {
var h264Options = {};
if (typeof self._initOptions.codecParams.video.h264.levelAsymmetryAllowed === 'string') {
h264Options['profile-level-id'] = self._initOptions.codecParams.video.h264.profileLevelId;
}
if (typeof self._initOptions.codecParams.video.h264.levelAsymmetryAllowed === 'boolean') {
h264Options['level-asymmetry-allowed'] = self._initOptions.codecParams.video.h264.levelAsymmetryAllowed;
}
if (typeof self._initOptions.codecParams.video.h264.packetizationMode === 'boolean') {
h264Options['packetization-mode'] = self._initOptions.codecParams.video.h264.packetizationMode;
}
// Possible future params (remove if they are decoder/encoder capabilities or info):
// max-recv-level, max-mbps, max-smbps, max-fs, max-cpb, max-dpb, max-br,
// max-mbps, max-smbps, max-fs, max-cpb, max-dpb, max-br, redundant-pic-cap, sprop-parameter-sets,
// sprop-level-parameter-sets, use-level-src-parameter-sets, in-band-parameter-sets,
// sprop-interleaving-depth, sprop-deint-buf-req, deint-buf-cap, sprop-init-buf-time,
// sprop-max-don-diff, max-rcmd-nalu-size, sar-understood, sar-supported
// NOT recommended: profile-level-id (WebRTC uses "42e00a" for the moment)
// https://bugs.chromium.org/p/chromium/issues/detail?id=645599
return h264Options;
})());
return sessionDescription.sdp;
};
/**
* Function that modifies the session description to limit the maximum sending bandwidth.
* Setting this may not necessarily work in Firefox.
* @method _setSDPBitrate
* @private
* @for Skylink
* @since 0.5.10
*/
Skylink.prototype._setSDPBitrate = function(targetMid, sessionDescription) {
var sdpLines = sessionDescription.sdp.split('\r\n');
var parseFn = function (type, bw) {
var mLineType = type;
var mLineIndex = -1;
var cLineIndex = -1;
if (type === 'data') {
mLineType = 'application';
}
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].indexOf('m=' + mLineType) === 0) {
mLineIndex = i;
} else if (mLineIndex > 0) {
if (sdpLines[i].indexOf('m=') === 0) {
break;
}
if (sdpLines[i].indexOf('c=') === 0) {
cLineIndex = i;
// Remove previous b:AS settings
} else if (sdpLines[i].indexOf('b=AS:') === 0 || sdpLines[i].indexOf('b:TIAS:') === 0) {
sdpLines.splice(i, 1);
i--;
}
}
}
if (!(typeof bw === 'number' && bw > 0)) {
log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Not limiting "' + type + '" bandwidth']);
return;
}
if (cLineIndex === -1) {
log.error([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Failed setting "' +
type + '" bandwidth as c-line is missing.']);
return;
}
// Follow RFC 4566, that the b-line should follow after c-line.
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Limiting maximum sending "' + type + '" bandwidth ->'], bw);
sdpLines.splice(cLineIndex + 1, 0, window.webrtcDetectedBrowser === 'firefox' ? 'b=TIAS:' + (bw * 1000 *
(window.webrtcDetectedVersion > 52 && window.webrtcDetectedVersion < 55 ? 1000 : 1)).toFixed(0) : 'b=AS:' + bw);
};
var bASAudioBw = this._streamsBandwidthSettings.bAS.audio;
var bASVideoBw = this._streamsBandwidthSettings.bAS.video;
var bASDataBw = this._streamsBandwidthSettings.bAS.data;
var googleXMinBw = this._streamsBandwidthSettings.googleX.min;
var googleXMaxBw = this._streamsBandwidthSettings.googleX.max;
if (this._peerCustomConfigs[targetMid]) {
if (this._peerCustomConfigs[targetMid].bandwidth &&
typeof this._peerCustomConfigs[targetMid].bandwidth === 'object') {
if (typeof this._peerCustomConfigs[targetMid].bandwidth.audio === 'number') {
bASAudioBw = this._peerCustomConfigs[targetMid].bandwidth.audio;
}
if (typeof this._peerCustomConfigs[targetMid].bandwidth.video === 'number') {
bASVideoBw = this._peerCustomConfigs[targetMid].bandwidth.video;
}
if (typeof this._peerCustomConfigs[targetMid].bandwidth.data === 'number') {
bASDataBw = this._peerCustomConfigs[targetMid].bandwidth.data;
}
}
if (this._peerCustomConfigs[targetMid].googleXBandwidth &&
typeof this._peerCustomConfigs[targetMid].googleXBandwidth === 'object') {
if (typeof this._peerCustomConfigs[targetMid].googleXBandwidth.min === 'number') {
googleXMinBw = this._peerCustomConfigs[targetMid].googleXBandwidth.min;
}
if (typeof this._peerCustomConfigs[targetMid].googleXBandwidth.max === 'number') {
googleXMaxBw = this._peerCustomConfigs[targetMid].googleXBandwidth.max;
}
}
}
parseFn('audio', bASAudioBw);
parseFn('video', bASVideoBw);
parseFn('data', bASDataBw);
// Sets the experimental google bandwidth
if ((typeof googleXMinBw === 'number') || (typeof googleXMaxBw === 'number')) {
var codec = null;
var codecRtpMapLineIndex = -1;
var codecFmtpLineIndex = -1;
for (var j = 0; j < sdpLines.length; j++) {
if (sdpLines[j].indexOf('m=video') === 0) {
codec = sdpLines[j].split(' ')[3];
} else if (codec) {
if (sdpLines[j].indexOf('m=') === 0) {
break;
}
if (sdpLines[j].indexOf('a=rtpmap:' + codec + ' ') === 0) {
codecRtpMapLineIndex = j;
} else if (sdpLines[j].indexOf('a=fmtp:' + codec + ' ') === 0) {
sdpLines[j] = sdpLines[j].replace(/x-google-(min|max)-bitrate=[0-9]*[;]*/gi, '');
codecFmtpLineIndex = j;
break;
}
}
}
if (codecRtpMapLineIndex > -1) {
var xGoogleParams = '';
if (typeof googleXMinBw === 'number') {
xGoogleParams += 'x-google-min-bitrate=' + googleXMinBw + ';';
}
if (typeof googleXMaxBw === 'number') {
xGoogleParams += 'x-google-max-bitrate=' + googleXMaxBw + ';';
}
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Limiting x-google-bitrate ->'], xGoogleParams);
if (codecFmtpLineIndex > -1) {
sdpLines[codecFmtpLineIndex] += (sdpLines[codecFmtpLineIndex].split(' ')[1] ? ';' : '') + xGoogleParams;
} else {
sdpLines.splice(codecRtpMapLineIndex + 1, 0, 'a=fmtp:' + codec + ' ' + xGoogleParams);
}
}
}
return sdpLines.join('\r\n');
};
/**
* Function that modifies the session description to set the preferred audio/video codec.
* @method _setSDPCodec
* @private
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype._setSDPCodec = function(targetMid, sessionDescription, overrideSettings) {
var self = this;
var parseFn = function (type, codecSettings) {
var codec = typeof codecSettings === 'object' ? codecSettings.codec : codecSettings;
var samplingRate = typeof codecSettings === 'object' ? codecSettings.samplingRate : null;
var channels = typeof codecSettings === 'object' ? codecSettings.channels : null;
if (codec === self[type === 'audio' ? 'AUDIO_CODEC' : 'VIDEO_CODEC'].AUTO) {
log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Not preferring any codec for "' + type + '" streaming. Using browser selection.']);
return;
}
var mLine = sessionDescription.sdp.match(new RegExp('m=' + type + ' .*\r\n', 'gi'));
if (!(Array.isArray(mLine) && mLine.length > 0)) {
log.error([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Not preferring any codec for "' + type + '" streaming as m= line is not found.']);
return;
}
var setLineFn = function (codecsList, isSROk, isChnlsOk) {
if (Array.isArray(codecsList) && codecsList.length > 0) {
if (!isSROk) {
samplingRate = null;
}
if (!isChnlsOk) {
channels = null;
}
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Preferring "' +
codec + '" (samplingRate: ' + (samplingRate || 'n/a') + ', channels: ' +
(channels || 'n/a') + ') for "' + type + '" streaming.']);
var line = mLine[0];
var lineParts = line.replace('\r\n', '').split(' ');
// Set the m=x x UDP/xxx
line = lineParts[0] + ' ' + lineParts[1] + ' ' + lineParts[2] + ' ';
// Remove them to leave the codecs only
lineParts.splice(0, 3);
// Loop for the codecs list to append first
for (var i = 0; i < codecsList.length; i++) {
var parts = (codecsList[i].split('a=rtpmap:')[1] || '').split(' ');
if (parts.length < 2) {
continue;
}
line += parts[0] + ' ';
}
// Loop for later fallback codecs to append
for (var j = 0; j < lineParts.length; j++) {
if (line.indexOf(' ' + lineParts[j]) > 0) {
lineParts.splice(j, 1);
j--;
} else if (sessionDescription.sdp.match(new RegExp('a=rtpmap:' + lineParts[j] +
'\ ' + codec + '/.*\r\n', 'gi'))) {
line += lineParts[j] + ' ';
lineParts.splice(j, 1);
j--;
}
}
// Append the rest of the codecs
line += lineParts.join(' ') + '\r\n';
sessionDescription.sdp = sessionDescription.sdp.replace(mLine[0], line);
return true;
}
};
// If samplingRate & channels
if (samplingRate) {
if (type === 'audio' && channels && setLineFn(sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' +
codec + '\/' + samplingRate + (channels === 1 ? '[\/1]*' : '\/' + channels) + '\r\n', 'gi')), true, true)) {
return;
} else if (setLineFn(sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' + codec + '\/' +
samplingRate + '[\/]*.*\r\n', 'gi')), true)) {
return;
}
}
if (type === 'audio' && channels && setLineFn(sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' +
codec + '\/.*\/' + channels + '\r\n', 'gi')), false, true)) {
return;
}
setLineFn(sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' + codec + '\/.*\r\n', 'gi')));
};
parseFn('audio', overrideSettings ? overrideSettings.audio : self._initOptions.audioCodec);
parseFn('video', overrideSettings ? overrideSettings.video : self._initOptions.videoCodec);
return sessionDescription.sdp;
};
/**
* Function that modifies the session description to remove the previous experimental H264
* codec that is apparently breaking connections.
* NOTE: We should perhaps not remove it since H264 is supported?
* @method _removeSDPFirefoxH264Pref
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._removeSDPFirefoxH264Pref = function(targetMid, sessionDescription) {
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Removing Firefox experimental H264 flag to ensure interopability reliability']);
return sessionDescription.sdp.replace(/a=fmtp:0 profile-level-id=0x42e00c;packetization-mode=1\r\n/g, '');
};
/**
* Function that modifies the session description to remove apt/rtx lines that does exists.
* @method _removeSDPUnknownAptRtx
* @private
* @for Skylink
* @since 0.6.18
*/
Skylink.prototype._removeSDPUnknownAptRtx = function (targetMid, sessionDescription) {
var codecsPayload = []; // m=audio 9 UDP/TLS/RTP/SAVPF [Start from index 3] 102 9 0 8 97 13 118 101
var sdpLines = sessionDescription.sdp.split('\r\n');
var mediaLines = sessionDescription.sdp.split('m=');
// Remove unmapped rtx lines
var formatRtx = function (str) {
(str.match(/a=rtpmap:.*\ rtx\/.*\r\n/gi) || []).forEach(function (line) {
var payload = (line.split('a=rtpmap:')[1] || '').split(' ')[0] || '';
var fmtpLine = (str.match(new RegExp('a=fmtp:' + payload + '\ .*\r\n', 'gi')) || [])[0];
if (!fmtpLine) {
str = str.replace(new RegExp(line, 'g'), '');
return;
}
var codecPayload = (fmtpLine.split(' apt=')[1] || '').replace(/\r\n/gi, '');
var rtmpLine = str.match(new RegExp('a=rtpmap:' + codecPayload + '\ .*\r\n', 'gi'));
if (!rtmpLine) {
str = str.replace(new RegExp(line, 'g'), '');
str = str.replace(new RegExp(fmtpLine, 'g'), '');
}
});
return str;
};
// Remove unmapped fmtp and rtcp-fb lines
var formatFmtpRtcpFb = function (str) {
(str.match(/a=(fmtp|rtcp-fb):.*\ rtx\/.*\r\n/gi) || []).forEach(function (line) {
var payload = (line.split('a=' + (line.indexOf('rtcp') > 0 ? 'rtcp-fb' : 'fmtp'))[1] || '').split(' ')[0] || '';
var rtmpLine = str.match(new RegExp('a=rtpmap:' + payload + '\ .*\r\n', 'gi'));
if (!rtmpLine) {
str = str.replace(new RegExp(line, 'g'), '');
}
});
return str;
};
// Remove rtx or apt= lines that prevent connections for browsers without VP8 or VP9 support
// See: https://bugs.chromium.org/p/webrtc/issues/detail?id=3962
for (var m = 0; m < mediaLines.length; m++) {
mediaLines[m] = formatRtx(mediaLines[m]);
mediaLines[m] = formatFmtpRtcpFb(mediaLines[m]);
}
return mediaLines.join('m=');
};
/**
* Function that modifies the session description to remove codecs.
* @method _removeSDPCodecs
* @private
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype._removeSDPCodecs = function (targetMid, sessionDescription) {
var audioSettings = this.getPeerInfo().settings.audio;
var parseFn = function (type, codec) {
var payloadList = sessionDescription.sdp.match(new RegExp('a=rtpmap:(\\d*)\\ ' + codec + '.*', 'gi'));
if (!(Array.isArray(payloadList) && payloadList.length > 0)) {
log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Not removing "' + codec + '" as it does not exists.']);
return;
}
for (var i = 0; i < payloadList.length; i++) {
var payload = payloadList[i].split(' ')[0].split(':')[1];
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Removing "' + codec + '" payload ->'], payload);
sessionDescription.sdp = sessionDescription.sdp.replace(
new RegExp('a=rtpmap:' + payload + '\\ .*\\r\\n', 'g'), '');
sessionDescription.sdp = sessionDescription.sdp.replace(
new RegExp('a=fmtp:' + payload + '\\ .*\\r\\n', 'g'), '');
sessionDescription.sdp = sessionDescription.sdp.replace(
new RegExp('a=rtpmap:\\d+ rtx\\/\\d+\\r\\na=fmtp:\\d+ apt=' + payload + '\\r\\n', 'g'), '');
// Remove the m-line codec
var sdpLines = sessionDescription.sdp.split('\r\n');
for (var j = 0; j < sdpLines.length; j++) {
if (sdpLines[j].indexOf('m=' + type) === 0) {
var parts = sdpLines[j].split(' ');
if (parts.indexOf(payload) >= 3) {
parts.splice(parts.indexOf(payload), 1);
}
sdpLines[j] = parts.join(' ');
break;
}
}
sessionDescription.sdp = sdpLines.join('\r\n');
}
};
if (this._initOptions.disableVideoFecCodecs) {
if (this._hasMCU) {
log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Not removing "ulpfec" or "red" codecs as connected to MCU to prevent connectivity issues.']);
} else {
parseFn('video', 'red');
parseFn('video', 'ulpfec');
}
}
if (this._initOptions.disableComfortNoiseCodec && audioSettings && typeof audioSettings === 'object' && audioSettings.stereo) {
parseFn('audio', 'CN');
}
if (window.webrtcDetectedBrowser === 'edge' &&
(((this._peerInformations[targetMid] || {}).agent || {}).name || 'unknown').name !== 'edge') {
sessionDescription.sdp = sessionDescription.sdp.replace(/a=rtcp-fb:.*\ x-message\ .*\r\n/gi, '');
}
return sessionDescription.sdp;
};
/**
* Function that modifies the session description to remove REMB packets fb.
* @method _removeSDPREMBPackets
* @private
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype._removeSDPREMBPackets = function (targetMid, sessionDescription) {
if (!this._initOptions.disableREMB) {
return sessionDescription.sdp;
}
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing REMB packets.']);
return sessionDescription.sdp.replace(/a=rtcp-fb:\d+ goog-remb\r\n/g, '');
};
/**
* Function that retrieves the session description selected codec.
* @method _getSDPSelectedCodec
* @private
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype._getSDPSelectedCodec = function (targetMid, sessionDescription, type, beSilentOnLogs) {
var codecInfo = {
name: null,
implementation: null,
clockRate: null,
channels: null,
payloadType: null,
params: null
};
if (!(sessionDescription && sessionDescription.sdp)) {
return codecInfo;
}
sessionDescription.sdp.split('m=').forEach(function (mediaItem, index) {
if (index === 0 || mediaItem.indexOf(type + ' ') !== 0) {
return;
}
var codecs = (mediaItem.split('\r\n')[0] || '').split(' ');
// Remove audio[0] 65266[1] UDP/TLS/RTP/SAVPF[2]
codecs.splice(0, 3);
for (var i = 0; i < codecs.length; i++) {
var match = mediaItem.match(new RegExp('a=rtpmap:' + codecs[i] + '.*\r\n', 'gi'));
if (!match) {
continue;
}
// Format: codec/clockRate/channels
var parts = ((match[0] || '').replace(/\r\n/g, '').split(' ')[1] || '').split('/');
// Ignore rtcp codecs, dtmf or comfort noise
if (['red', 'ulpfec', 'telephone-event', 'cn', 'rtx'].indexOf(parts[0].toLowerCase()) > -1) {
continue;
}
codecInfo.name = parts[0];
codecInfo.clockRate = parseInt(parts[1], 10) || 0;
codecInfo.channels = parseInt(parts[2] || '1', 10) || 1;
codecInfo.payloadType = parseInt(codecs[i], 10);
codecInfo.params = '';
// Get the list of codec parameters
var params = mediaItem.match(new RegExp('a=fmtp:' + codecs[i] + '.*\r\n', 'gi')) || [];
params.forEach(function (paramItem) {
codecInfo.params += paramItem.replace(new RegExp('a=fmtp:' + codecs[i], 'gi'), '').replace(/\ /g, '').replace(/\r\n/g, '');
});
break;
}
});
if (!beSilentOnLogs) {
log.debug([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Parsing session description "' + type + '" codecs ->'], codecInfo);
}
return codecInfo;
};
/**
* Function that modifies the session description to remove non-relay ICE candidates.
* @method _removeSDPFilteredCandidates
* @private
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype._removeSDPFilteredCandidates = function (targetMid, sessionDescription) {
// Handle Firefox MCU Peer ICE candidates
if (targetMid === 'MCU' && sessionDescription.type === this.HANDSHAKE_PROGRESS.ANSWER &&
window.webrtcDetectedBrowser === 'firefox') {
sessionDescription.sdp = sessionDescription.sdp.replace(/ generation 0/g, '');
sessionDescription.sdp = sessionDescription.sdp.replace(/ udp /g, ' UDP ');
}
if (this._initOptions.forceTURN && this._hasMCU) {
log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Not filtering ICE candidates as ' +
'TURN connections are enforced as MCU is present (and act as a TURN itself) so filtering of ICE candidate ' +
'flags are not honoured']);
return sessionDescription.sdp;
}
if (this._initOptions.filterCandidatesType.host) {
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing "host" ICE candidates.']);
sessionDescription.sdp = sessionDescription.sdp.replace(/a=candidate:.*host.*\r\n/g, '');
}
if (this._initOptions.filterCandidatesType.srflx) {
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing "srflx" ICE candidates.']);
sessionDescription.sdp = sessionDescription.sdp.replace(/a=candidate:.*srflx.*\r\n/g, '');
}
if (this._initOptions.filterCandidatesType.relay) {
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing "relay" ICE candidates.']);
sessionDescription.sdp = sessionDescription.sdp.replace(/a=candidate:.*relay.*\r\n/g, '');
}
// sessionDescription.sdp = sessionDescription.sdp.replace(/a=candidate:(?!.*relay.*).*\r\n/g, '');
return sessionDescription.sdp;
};
/**
* Function that retrieves the current list of support codecs.
* @method _getCodecsSupport
* @private
* @for Skylink
* @since 0.6.18
*/
Skylink.prototype._getCodecsSupport = function (callback) {
var self = this;
if (self._currentCodecSupport) {
callback(null);
return;
}
self._currentCodecSupport = { audio: {}, video: {} };
// Safari 11 REQUIRES a stream first before connection works, hence let's spoof it for now
if (AdapterJS.webrtcDetectedType === 'AppleWebKit') {
self._currentCodecSupport.audio = {
opus: ['48000/2']
};
self._currentCodecSupport.video = {
h264: ['48000']
};
return callback(null);
}
try {
if (window.webrtcDetectedBrowser === 'edge') {
var codecs = RTCRtpSender.getCapabilities().codecs;
for (var i = 0; i < codecs.length; i++) {
if (['audio','video'].indexOf(codecs[i].kind) > -1 && codecs[i].name) {
var codec = codecs[i].name.toLowerCase();
self._currentCodecSupport[codecs[i].kind][codec] = codecs[i].clockRate +
(codecs[i].numChannels > 1 ? '/' + codecs[i].numChannels : '');
}
}
// Ignore .fecMechanisms for now
callback(null);
} else {
var pc = new RTCPeerConnection(null);
var offerConstraints = AdapterJS.webrtcDetectedType !== 'plugin' ? {
offerToReceiveAudio: true,
offerToReceiveVideo: true
} : {
mandatory: {
OfferToReceiveVideo: true,
OfferToReceiveAudio: true
}
};
// Prevent errors and proceed with create offer still...
try {
var channel = pc.createDataChannel('test');
self._binaryChunkType = channel.binaryType || self._binaryChunkType;
self._binaryChunkType = self._binaryChunkType.toLowerCase().indexOf('array') > -1 ?
self.DATA_TRANSFER_DATA_TYPE.ARRAY_BUFFER : self._binaryChunkType;
// Set the value according to the property
for (var prop in self.DATA_TRANSFER_DATA_TYPE) {
if (self.DATA_TRANSFER_DATA_TYPE.hasOwnProperty(prop) &&
self._binaryChunkType.toLowerCase() === self.DATA_TRANSFER_DATA_TYPE[prop].toLowerCase()) {
self._binaryChunkType = self.DATA_TRANSFER_DATA_TYPE[prop];
break;
}
}
} catch (e) {}
pc.createOffer(function (offer) {
self._currentCodecSupport = self._getSDPCodecsSupport(null, offer);
callback(null);
}, function (error) {
callback(error);
}, offerConstraints);
}
} catch (error) {
callback(error);
}
};
/**
* Function that modifies the session description to handle the connection settings.
* This is experimental and never recommended to end-users.
* @method _handleSDPConnectionSettings
* @private
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype._handleSDPConnectionSettings = function (targetMid, sessionDescription, direction) {
var self = this;
if (!self._sdpSessions[targetMid]) {
return sessionDescription.sdp;
}
var sessionDescriptionStr = sessionDescription.sdp;
// Handle a=end-of-candidates signaling for non-trickle ICE before setting remote session description
if (direction === 'remote' && !self.getPeerInfo(targetMid).config.enableIceTrickle) {
sessionDescriptionStr = sessionDescriptionStr.replace(/a=end-of-candidates\r\n/g, '');
}
var sdpLines = sessionDescriptionStr.split('\r\n');
var peerAgent = ((self._peerInformations[targetMid] || {}).agent || {}).name || '';
var peerVersion = ((self._peerInformations[targetMid] || {}).agent || {}).version || 0;
var mediaType = '';
var bundleLineIndex = -1;
var bundleLineMids = [];
var mLineIndex = -1;
var settings = clone(self._sdpSettings);
if (targetMid === 'MCU') {
settings.connection.audio = true;
settings.connection.video = true;
settings.connection.data = true;
}
// Patches for MCU sending empty video stream despite audio+video is not sending at all
// Apply as a=inactive when supported
if (self._hasMCU) {
var peerStreamSettings = clone(self.getPeerInfo(targetMid)).settings || {};
settings.direction.audio.receive = targetMid === 'MCU' ? true : !!peerStreamSettings.audio;
settings.direction.audio.send = targetMid === 'MCU' ? true : false;
settings.direction.video.receive = targetMid === 'MCU' ? true : !!peerStreamSettings.video;
settings.direction.video.send = targetMid === 'MCU' ? true : false;
}
if (direction === 'remote') {
var offerCodecs = self._getSDPCommonSupports(targetMid, sessionDescription);
if (!offerCodecs.audio) {
settings.connection.audio = false;
}
if (!offerCodecs.video) {
settings.connection.video = false;
}
}
// ANSWERER: Reject only the m= lines. Returned rejected m= lines as well.
// OFFERER: Remove m= lines
self._sdpSessions[targetMid][direction].mLines = [];
self._sdpSessions[targetMid][direction].bundleLine = '';
self._sdpSessions[targetMid][direction].connection = {
audio: null,
video: null,
data: null
};
for (var i = 0; i < sdpLines.length; i++) {
// Cache the a=group:BUNDLE line used for remote answer from Edge later
if (sdpLines[i].indexOf('a=group:BUNDLE') === 0) {
self._sdpSessions[targetMid][direction].bundleLine = sdpLines[i];
bundleLineIndex = i;
// Check if there's a need to reject m= line
} else if (sdpLines[i].indexOf('m=') === 0) {
mediaType = (sdpLines[i].split('m=')[1] || '').split(' ')[0] || '';
mediaType = mediaType === 'application' ? 'data' : mediaType;
mLineIndex++;
self._sdpSessions[targetMid][direction].mLines[mLineIndex] = sdpLines[i];
// Check if there is missing unsupported video codecs support and reject it regardles of MCU Peer or not
if (!settings.connection[mediaType]) {
log.log([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Removing rejected m=' + mediaType + ' line ->'], sdpLines[i]);
// Check if answerer and we do not have the power to remove the m line if index is 0
// Set as a=inactive because we do not have that power to reject it somehow..
// first m= line cannot be rejected for BUNDLE
if (self._peerConnectionConfig.bundlePolicy === self.BUNDLE_POLICY.MAX_BUNDLE &&
bundleLineIndex > -1 && mLineIndex === 0 && (direction === 'remote' ?
sessionDescription.type === this.HANDSHAKE_PROGRESS.OFFER :
sessionDescription.type === this.HANDSHAKE_PROGRESS.ANSWER)) {
log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Not removing rejected m=' + mediaType + ' line ->'], sdpLines[i]);
settings.connection[mediaType] = true;
if (['audio', 'video'].indexOf(mediaType) > -1) {
settings.direction[mediaType].send = false;
settings.direction[mediaType].receive = false;
}
continue;
}
if (window.webrtcDetectedBrowser === 'edge') {
sdpLines.splice(i, 1);
i--;
continue;
} else if (direction === 'remote' || sessionDescription.type === this.HANDSHAKE_PROGRESS.ANSWER) {
var parts = sdpLines[i].split(' ');
parts[1] = 0;
sdpLines[i] = parts.join(' ');
continue;
}
}
}
if (direction === 'remote' && sdpLines[i].indexOf('a=candidate:') === 0 &&
!self.getPeerInfo(targetMid).config.enableIceTrickle) {
if (sdpLines[i + 1] ? !(sdpLines[i + 1].indexOf('a=candidate:') === 0 ||
sdpLines[i + 1].indexOf('a=end-of-candidates') === 0) : true) {
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Appending end-of-candidates signal for non-trickle ICE connection.']);
sdpLines.splice(i + 1, 0, 'a=end-of-candidates');
i++;
}
}
if (mediaType) {
// Remove lines if we are rejecting the media and ensure unless (rejectVideoMedia is true), MCU has to enable those m= lines
if (!settings.connection[mediaType]) {
sdpLines.splice(i, 1);
i--;
// Store the mids session description
} else if (sdpLines[i].indexOf('a=mid:') === 0) {
bundleLineMids.push(sdpLines[i].split('a=mid:')[1] || '');
// Configure direction a=sendonly etc for local sessiondescription
} else if (mediaType && ['a=sendrecv', 'a=sendonly', 'a=recvonly'].indexOf(sdpLines[i]) > -1) {
if (['audio', 'video'].indexOf(mediaType) === -1) {
self._sdpSessions[targetMid][direction].connection.data = sdpLines[i];
continue;
}
if (direction === 'local') {
if (settings.direction[mediaType].send && !settings.direction[mediaType].receive) {
sdpLines[i] = sdpLines[i].indexOf('send') > -1 ? 'a=sendonly' : 'a=inactive';
} else if (!settings.direction[mediaType].send && settings.direction[mediaType].receive) {
sdpLines[i] = sdpLines[i].indexOf('recv') > -1 ? 'a=recvonly' : 'a=inactive';
} else if (!settings.direction[mediaType].send && !settings.direction[mediaType].receive) {
// MCU currently does not support a=inactive flag.. what do we do here?
sdpLines[i] = 'a=inactive';
}
// Handle Chrome bundle bug. - See: https://bugs.chromium.org/p/webrtc/issues/detail?id=6280
if (!self._hasMCU && window.webrtcDetectedBrowser !== 'firefox' && peerAgent === 'firefox' &&
sessionDescription.type === self.HANDSHAKE_PROGRESS.OFFER && sdpLines[i] === 'a=recvonly') {
log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Overriding any original settings ' +
'to receive only to send and receive to resolve chrome BUNDLE errors.']);
sdpLines[i] = 'a=sendrecv';
settings.direction[mediaType].send = true;
settings.direction[mediaType].receive = true;
}
// Patch for incorrect responses
} else if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER) {
var localOfferRes = self._sdpSessions[targetMid].local.connection[mediaType];
// Parse a=sendonly response
if (localOfferRes === 'a=sendonly') {
sdpLines[i] = ['a=inactive', 'a=recvonly'].indexOf(sdpLines[i]) === -1 ?
(sdpLines[i] === 'a=sendonly' ? 'a=inactive' : 'a=recvonly') : sdpLines[i];
// Parse a=recvonly
} else if (localOfferRes === 'a=recvonly') {
sdpLines[i] = ['a=inactive', 'a=sendonly'].indexOf(sdpLines[i]) === -1 ?
(sdpLines[i] === 'a=recvonly' ? 'a=inactive' : 'a=sendonly') : sdpLines[i];
// Parse a=sendrecv
} else if (localOfferRes === 'a=inactive') {
sdpLines[i] = 'a=inactive';
}
}
self._sdpSessions[targetMid][direction].connection[mediaType] = sdpLines[i];
}
}
// Remove weird empty characters for Edge case.. :(
if (!(sdpLines[i] || '').replace(/\n|\r|\s|\ /gi, '')) {
sdpLines.splice(i, 1);
i--;
}
}
// Fix chrome "offerToReceiveAudio" local offer not removing audio BUNDLE
if (bundleLineIndex > -1) {
if (self._peerConnectionConfig.bundlePolicy === self.BUNDLE_POLICY.MAX_BUNDLE) {
sdpLines[bundleLineIndex] = 'a=group:BUNDLE ' + bundleLineMids.join(' ');
// Remove a=group:BUNDLE line
} else if (self._peerConnectionConfig.bundlePolicy === self.BUNDLE_POLICY.NONE) {
sdpLines.splice(bundleLineIndex, 1);
}
}
// Append empty space below
if (window.webrtcDetectedBrowser !== 'edge') {
if (!sdpLines[sdpLines.length - 1].replace(/\n|\r|\s/gi, '')) {
sdpLines[sdpLines.length - 1] = '';
} else {
sdpLines.push('');
}
}
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Handling connection lines and direction ->'], settings);
return sdpLines.join('\r\n');
};
/**
* Function that parses and retrieves the session description fingerprint.
* @method _getSDPFingerprint
* @private
* @for Skylink
* @since 0.6.18
*/
Skylink.prototype._getSDPFingerprint = function (targetMid, sessionDescription, beSilentOnLogs) {
var fingerprint = {
fingerprint: null,
fingerprintAlgorithm: null,
derBase64: null
};
if (!(sessionDescription && sessionDescription.sdp)) {
return fingerprint;
}
var sdpLines = sessionDescription.sdp.split('\r\n');
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].indexOf('a=fingerprint') === 0) {
var parts = sdpLines[i].replace('a=fingerprint:', '').split(' ');
fingerprint.fingerprint = parts[1];
fingerprint.fingerprintAlgorithm = parts[0];
break;
}
}
if (!beSilentOnLogs) {
log.debug([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Parsing session description fingerprint ->'], fingerprint);
}
return fingerprint;
};
/**
* Function that modifies the session description to append the MediaStream and MediaStreamTrack IDs that seems
* to be missing from Firefox answer session description to Chrome connection causing freezes in re-negotiation.
* @method _renderSDPOutput
* @private
* @for Skylink
* @since 0.6.25
*/
Skylink.prototype._renderSDPOutput = function (targetMid, sessionDescription) {
var self = this;
var localStream = null;
var localStreamId = null;
if (!(sessionDescription && sessionDescription.sdp)) {
return;
}
if (!self._peerConnections[targetMid]) {
return sessionDescription.sdp;
}
if (self._peerConnections[targetMid].localStream) {
localStream = self._peerConnections[targetMid].localStream;
localStreamId = self._peerConnections[targetMid].localStreamId || self._peerConnections[targetMid].localStream.id;
}
// For non-trickle ICE, remove the a=end-of-candidates line first to append it properly later
var sdpLines = (!self._initOptions.enableIceTrickle ? sessionDescription.sdp.replace(/a=end-of-candidates\r\n/g, '') : sessionDescription.sdp).split('\r\n');
var agent = ((self._peerInformations[targetMid] || {}).agent || {}).name || '';
// Parse and replace with the correct msid to prevent unwanted streams.
// Making it simple without replacing with the track IDs or labels, neither setting prefixing "mslabel" and "label" as required labels.
if (localStream) {
var ssrcId = null;
var mediaType = '';
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].indexOf('m=') === 0) {
mediaType = (sdpLines[i].split('m=')[1] || '').split(' ')[0] || '';
mediaType = ['audio', 'video'].indexOf(mediaType) === -1 ? '' : mediaType;
} else if (mediaType) {
if (sdpLines[i].indexOf('a=msid:') === 0) {
var msidParts = sdpLines[i].split(' ');
msidParts[0] = 'a=msid:' + localStreamId;
sdpLines[i] = msidParts.join(' ');
} else if (sdpLines[i].indexOf('a=ssrc:') === 0) {
var ssrcParts = null;
// Replace for "msid:" and "mslabel:"
if (sdpLines[i].indexOf(' msid:') > 0) {
ssrcParts = sdpLines[i].split(' msid:');
} else if (sdpLines[i].indexOf(' mslabel:') > 0) {
ssrcParts = sdpLines[i].split(' mslabel:');
}
if (ssrcParts) {
var ssrcMsidParts = (ssrcParts[1] || '').split(' ');
ssrcMsidParts[0] = localStreamId;
ssrcParts[1] = ssrcMsidParts.join(' ');
if (sdpLines[i].indexOf(' msid:') > 0) {
sdpLines[i] = ssrcParts.join(' msid:');
} else if (sdpLines[i].indexOf(' mslabel:') > 0) {
sdpLines[i] = ssrcParts.join(' mslabel:');
}
}
}
}
}
}
// For non-trickle ICE, append the signaling of end-of-candidates properly
if (!self._initOptions.enableIceTrickle){
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Appending end-of-candidates signal for non-trickle ICE connection.']);
for (var e = 0; e < sdpLines.length; e++) {
if (sdpLines[e].indexOf('a=candidate:') === 0) {
if (sdpLines[e + 1] ? !(sdpLines[e + 1].indexOf('a=candidate:') === 0 ||
sdpLines[e + 1].indexOf('a=end-of-candidates') === 0) : true) {
sdpLines.splice(e + 1, 0, 'a=end-of-candidates');
e++;
}
}
}
}
// Replace the bundle policy to prevent complete removal of m= lines for some cases that do not accept missing m= lines except edge.
if (sessionDescription.type === this.HANDSHAKE_PROGRESS.ANSWER && this._sdpSessions[targetMid]) {
var bundleLineIndex = -1;
var mLineIndex = -1;
for (var j = 0; j < sdpLines.length; j++) {
if (sdpLines[j].indexOf('a=group:BUNDLE') === 0 && this._sdpSessions[targetMid].remote.bundleLine &&
this._peerConnectionConfig.bundlePolicy === this.BUNDLE_POLICY.MAX_BUNDLE) {
sdpLines[j] = this._sdpSessions[targetMid].remote.bundleLine;
} else if (sdpLines[j].indexOf('m=') === 0) {
mLineIndex++;
var compareA = sdpLines[j].split(' ');
var compareB = (this._sdpSessions[targetMid].remote.mLines[mLineIndex] || '').split(' ');
if (compareA[0] && compareB[0] && compareA[0] !== compareB[0]) {
compareB[1] = 0;
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Appending middle rejected m= line ->'], compareB.join(' '));
sdpLines.splice(j, 0, compareB.join(' '));
j++;
mLineIndex++;
}
}
}
while (this._sdpSessions[targetMid].remote.mLines[mLineIndex + 1]) {
mLineIndex++;
var appendIndex = sdpLines.length;
if (!sdpLines[appendIndex - 1].replace(/\s/gi, '')) {
appendIndex -= 1;
}
var parts = (this._sdpSessions[targetMid].remote.mLines[mLineIndex] || '').split(' ');
parts[1] = 0;
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Appending later rejected m= line ->'], parts.join(' '));
sdpLines.splice(appendIndex, 0, parts.join(' '));
}
}
// Ensure for chrome case to have empty "" at last line or it will return invalid SDP errors
if (window.webrtcDetectedBrowser === 'edge' && sessionDescription.type === this.HANDSHAKE_PROGRESS.OFFER &&
!sdpLines[sdpLines.length - 1].replace(/\s/gi, '')) {
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing last empty space for Edge browsers']);
sdpLines.splice(sdpLines.length - 1, 1);
}
/*
var outputStr = sdpLines.join('\r\n');
if (window.webrtcDetectedBrowser === 'edge' && this._streams.userMedia && this._streams.userMedia.stream) {
var correctStreamId = this._streams.userMedia.stream.id || this._streams.userMedia.stream.label;
outputStr = outputStr.replace(new RegExp('a=msid:.*\ ', 'gi'), 'a=msid:' + correctStreamId + ' ');
outputStr = outputStr.replace(new RegExp('\ msid:.*\ ', 'gi'), ' msid:' + correctStreamId + ' ');
}*/
log.info([targetMid, 'RTCSessionDescription', sessionDescription.type, 'Formatted output ->'], sdpLines.join('\r\n'));
return sdpLines.join('\r\n');
};
/**
* Function that parses the session description to get the MediaStream IDs.
* NOTE: It might not completely accurate if the setRemoteDescription() fails..
* @method _parseSDPMediaStreamIDs
* @private
* @for Skylink
* @since 0.6.25
*/
Skylink.prototype._parseSDPMediaStreamIDs = function (targetMid, sessionDescription) {
if (!this._peerConnections[targetMid]) {
return;
}
if (!(sessionDescription && sessionDescription.sdp)) {
this._peerConnections[targetMid].remoteStreamId = null;
return;
}
var sdpLines = sessionDescription.sdp.split('\r\n');
var currentStreamId = null;
for (var i = 0; i < sdpLines.length; i++) {
// a=msid:{31145dc5-b3e2-da4c-a341-315ef3ebac6b} {e0cac7dd-64a0-7447-b719-7d5bf042ca05}
if (sdpLines[i].indexOf('a=msid:') === 0) {
currentStreamId = (sdpLines[i].split('a=msid:')[1] || '').split(' ')[0];
break;
// a=ssrc:691169016 msid:c58721ed-b7db-4e7c-ac37-47432a7a2d6f 2e27a4b8-bc74-4118-b3d4-0f1c4ed4869b
} else if (sdpLines[i].indexOf('a=ssrc:') === 0 && sdpLines[i].indexOf(' msid:') > 0) {
currentStreamId = (sdpLines[i].split(' msid:')[1] || '').split(' ')[0];
break;
}
}
// No stream set
if (!currentStreamId) {
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'No remote stream is sent.']);
this._peerConnections[targetMid].remoteStreamId = null;
// New stream set
} else if (currentStreamId !== this._peerConnections[targetMid].remoteStreamId) {
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'New remote stream is sent ->'], currentStreamId);
this._peerConnections[targetMid].remoteStreamId = currentStreamId;
// Same stream set
} else {
log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Same remote stream is sent ->'], currentStreamId);
}
};
/**
* Function that parses and retrieves the session description ICE candidates.
* @method _getSDPICECandidates
* @private
* @for Skylink
* @since 0.6.18
*/
Skylink.prototype._getSDPICECandidates = function (targetMid, sessionDescription, beSilentOnLogs) {
var candidates = {
host: [],
srflx: [],
relay: []
};
if (!(sessionDescription && sessionDescription.sdp)) {
return candidates;
}
sessionDescription.sdp.split('m=').forEach(function (mediaItem, index) {
// Ignore the v=0 lines etc..
if (index === 0) {
return;
}
// Remove a=mid: and \r\n
var sdpMid = ((mediaItem.match(/a=mid:.*\r\n/gi) || [])[0] || '').replace(/a=mid:/gi, '').replace(/\r\n/, '');
var sdpMLineIndex = index - 1;
(mediaItem.match(/a=candidate:.*\r\n/gi) || []).forEach(function (item) {
// Remove \r\n for candidate type being set at the end of candidate DOM string.
var canType = (item.split(' ')[7] || 'host').replace(/\r\n/g, '');
candidates[canType] = candidates[canType] || [];
candidates[canType].push(new RTCIceCandidate({
sdpMid: sdpMid,
sdpMLineIndex: sdpMLineIndex,
// Remove initial "a=" in a=candidate
candidate: (item.split('a=')[1] || '').replace(/\r\n/g, '')
}));
});
});
if (!beSilentOnLogs) {
log.debug([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Parsing session description ICE candidates ->'], candidates);
}
return candidates;
};
/**
* Function that gets each media line SSRCs.
* @method _getSDPMediaSSRC
* @private
* @for Skylink
* @since 0.6.18
*/
Skylink.prototype._getSDPMediaSSRC = function (targetMid, sessionDescription, beSilentOnLogs) {
var ssrcs = {
audio: 0,
video: 0
};
if (!(sessionDescription && sessionDescription.sdp)) {
return ssrcs;
}
sessionDescription.sdp.split('m=').forEach(function (mediaItem, index) {
// Ignore the v=0 lines etc..
if (index === 0) {
return;
}
var mediaType = (mediaItem.split(' ')[0] || '');
var ssrcLine = (mediaItem.match(/a=ssrc:.*\r\n/) || [])[0];
if (typeof ssrcs[mediaType] !== 'number') {
return;
}
if (ssrcLine) {
ssrcs[mediaType] = parseInt((ssrcLine.split('a=ssrc:')[1] || '').split(' ')[0], 10) || 0;
}
});
if (!beSilentOnLogs) {
log.debug([targetMid, 'RTCSessionDesription', sessionDescription.type,
'Parsing session description media SSRCs ->'], ssrcs);
}
return ssrcs;
};
/**
* Function that parses the current list of supported codecs from session description.
* @method _getSDPCodecsSupport
* @private
* @for Skylink
* @since 0.6.18
*/
Skylink.prototype._getSDPCodecsSupport = function (targetMid, sessionDescription) {
var self = this;
var codecs = {
audio: {},
video: {}
};
if (!(sessionDescription && sessionDescription.sdp)) {
return codecs;
}
var sdpLines = sessionDescription.sdp.split('\r\n');
var mediaType = '';
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].indexOf('m=') === 0) {
mediaType = (sdpLines[i].split('m=')[1] || '').split(' ')[0];
continue;
}
if (sdpLines[i].indexOf('a=rtpmap:') === 0) {
var parts = (sdpLines[i].split(' ')[1] || '').split('/');
var codec = (parts[0] || '').toLowerCase();
var info = parts[1] + (parts[2] ? '/' + parts[2] : '');
if (['ulpfec', 'red', 'telephone-event', 'cn', 'rtx'].indexOf(codec) > -1) {
continue;
}
codecs[mediaType][codec] = codecs[mediaType][codec] || [];
if (codecs[mediaType][codec].indexOf(info) === -1) {
codecs[mediaType][codec].push(info);
}
}
}
log.info([targetMid || null, 'RTCSessionDescription', sessionDescription.type, 'Parsed codecs support ->'], codecs);
return codecs;
};
/**
* Function that checks if there are any common codecs supported for remote end.
* @method _getSDPCommonSupports
* @private
* @for Skylink
* @since 0.6.25
*/
Skylink.prototype._getSDPCommonSupports = function (targetMid, sessionDescription) {
var self = this;
var offer = {
audio: false,
video: false
};
if (!targetMid || !(sessionDescription && sessionDescription.sdp)) {
offer.video = !!(self._currentCodecSupport.video.h264 || self._currentCodecSupport.video.vp8);
offer.audio = !!self._currentCodecSupport.audio.opus;
if (targetMid) {
var peerAgent = ((self._peerInformations[targetMid] || {}).agent || {}).name || '';
if (AdapterJS.webrtcDetectedBrowser === peerAgent) {
offer.video = Object.keys(self._currentCodecSupport.video).length > 0;
offer.audio = Object.keys(self._currentCodecSupport.audio).length > 0;
}
}
return offer;
}
var remoteCodecs = self._getSDPCodecsSupport(targetMid, sessionDescription);
var localCodecs = self._currentCodecSupport;
for (var ac in localCodecs.audio) {
if (localCodecs.audio.hasOwnProperty(ac) && localCodecs.audio[ac] && remoteCodecs.audio[ac]) {
offer.audio = true;
break;
}
}
for (var vc in localCodecs.video) {
if (localCodecs.video.hasOwnProperty(vc) && localCodecs.video[vc] && remoteCodecs.video[vc]) {
offer.video = true;
break;
}
}
return offer;
};
/**
* Function adds SCTP port number for Firefox 63.0.3 and above if its missing in the answer from MCU
* @method _setSCTPport
* @private
* @for Skylink
* @since 0.6.35
*/
Skylink.prototype._setSCTPport = function (targetMid, sessionDescription) {
var self = this;
if (AdapterJS.webrtcDetectedBrowser === 'firefox' && AdapterJS.webrtcDetectedVersion >= 63 && self._hasMCU === true) {
var sdpLines = sessionDescription.sdp.split('\r\n');
var mLineType = 'application';
var mLineIndex = -1;
var sdpType = sessionDescription.type;
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].indexOf('m=' + mLineType) === 0) {
mLineIndex = i;
} else if (mLineIndex > 0) {
if (sdpLines[i].indexOf('m=') === 0) {
break;
}
// Saving m=application line when creating offer into instance variable
if (sdpType === 'offer') {
self._mline = sdpLines[mLineIndex];
break;
}
// Replacing m=application line from instance variable
if (sdpType === 'answer') {
sdpLines[mLineIndex] = self._mline;
sdpLines.splice(mLineIndex + 1, 0, 'a=sctp-port:5000');
break;
}
}
}
return sdpLines.join('\r\n');
}
return sessionDescription.sdp;
};
/**
* Function sets the original DTLS role which was negotiated on first offer/ansswer exchange
* This needs to be done until https://bugzilla.mozilla.org/show_bug.cgi?id=1240897 is released in Firefox 68
* Estimated release date for Firefox 68 : 2019-07-09 (https://wiki.mozilla.org/Release_Management/Calendar)
* @method _setOriginalDTLSRole
* @private
* @for Skylink
* @since 0.6.35
*/
Skylink.prototype._setOriginalDTLSRole = function (sessionDescription, isRemote) {
var self = this;
var sdpType = sessionDescription.type;
var role = null;
var aSetupPattern = null;
var invertRoleMap = { active: 'passive', passive: 'active' };
if (self._originalDTLSRole !== null || sdpType === 'offer') {
return;
}
aSetupPattern = sessionDescription.sdp.match(/a=setup:([a-z]+)/);
if (!aSetupPattern) {
return;
}
role = aSetupPattern[1];
self._originalDTLSRole = isRemote ? invertRoleMap[role] : role;
};
/**
* Function that modifies the DTLS role in answer sdp
* This needs to be done until https://bugzilla.mozilla.org/show_bug.cgi?id=1240897 is released in Firefox 68
* Estimated release date for Firefox 68 : 2019-07-09 (https://wiki.mozilla.org/Release_Management/Calendar)
* @method _modifyDTLSRole
* @private
* @for Skylink
* @since 1.0.0
*/
Skylink.prototype._modifyDTLSRole = function (sessionDescription) {
var self = this;
var sdpType = sessionDescription.type;
if (self._originalDTLSRole === null || sdpType === 'offer') {
return;
}
sessionDescription.sdp = sessionDescription.sdp.replace(/a=setup:[a-z]+/g, 'a=setup:' + self._originalDTLSRole);
return sessionDescription.sdp;
};