/* global __filename, mozRTCPeerConnection, webkitRTCPeerConnection,
RTCPeerConnection, RTCSessionDescription */
import { getLogger } from "jitsi-meet-logger";
import * as GlobalOnErrorHandler from "../util/GlobalOnErrorHandler";
import RTC from "./RTC";
import RTCBrowserType from "./RTCBrowserType.js";
import RTCEvents from "../../service/RTC/RTCEvents";
import RtxModifier from "../xmpp/RtxModifier.js";
// FIXME SDP tools should end up in some kind of util module
import SDP from "../xmpp/SDP";
import SdpConsistency from "../xmpp/SdpConsistency.js";
import SDPUtil from "../xmpp/SDPUtil";
import transform from "sdp-transform";
const logger = getLogger(__filename);
const SIMULCAST_LAYERS = 3;
/**
* Creates new instance of 'TraceablePeerConnection'.
*
* @param {RTC} rtc the instance of RTC service
* @param {number} id the peer connection id assigned by the parent RTC module.
* @param {SignalingLayer} signalingLayer the signaling layer instance
* @param {object} ice_config WebRTC 'PeerConnection' ICE config
* @param {object} constraints WebRTC 'PeerConnection' constraints
* @param {object} options TracablePeerConnection config options.
* @param {boolean} options.disableSimulcast if set to 'true' will disable
* the simulcast
* @param {boolean} options.disableRtx if set to 'true' will disable the RTX
* @param {boolean} options.preferH264 if set to 'true' H264 will be preferred
* over other video codecs.
*
* FIXME: initially the purpose of TraceablePeerConnection was to be able to
* debug the peer connection. Since many other responsibilities have been added
* it would make sense to extract a separate class from it and come up with
* a more suitable name.
*
* @constructor
*/
function TraceablePeerConnection(rtc, id, signalingLayer, ice_config,
constraints, options) {
var self = this;
/**
* The parent instance of RTC service which created this
* TracablePeerConnection.
* @type {RTC}
*/
this.rtc = rtc;
/**
* The peer connection identifier assigned by the RTC module.
* @type {number}
*/
this.id = id;
/**
* The signaling layer which operates this peer connection.
* @type {SignalingLayer}
*/
this.signalingLayer = signalingLayer;
this.options = options;
var RTCPeerConnectionType = null;
if (RTCBrowserType.isFirefox()) {
RTCPeerConnectionType = mozRTCPeerConnection;
} else if (RTCBrowserType.isTemasysPluginUsed()) {
RTCPeerConnectionType = RTCPeerConnection;
} else {
RTCPeerConnectionType = webkitRTCPeerConnection;
}
this.peerconnection = new RTCPeerConnectionType(ice_config, constraints);
this.updateLog = [];
this.stats = {};
this.statsinterval = null;
/**
* @type {number}
*/
this.maxstats = 0;
var Interop = require('sdp-interop').Interop;
this.interop = new Interop();
var Simulcast = require('sdp-simulcast');
this.simulcast = new Simulcast({numOfLayers: SIMULCAST_LAYERS,
explodeRemoteSimulcast: false});
this.sdpConsistency = new SdpConsistency();
/**
* TracablePeerConnection uses RTC's eventEmitter
* @type {EventEmitter}
*/
this.eventEmitter = rtc.eventEmitter;
this.rtxModifier = new RtxModifier();
// override as desired
this.trace = function (what, info) {
/*logger.warn('WTRACE', what, info);
if (info && RTCBrowserType.isIExplorer()) {
if (info.length > 1024) {
logger.warn('WTRACE', what, info.substr(1024));
}
if (info.length > 2048) {
logger.warn('WTRACE', what, info.substr(2048));
}
}*/
self.updateLog.push({
time: new Date(),
type: what,
value: info || ""
});
};
this.onicecandidate = null;
this.peerconnection.onicecandidate = function (event) {
// FIXME: this causes stack overflow with Temasys Plugin
if (!RTCBrowserType.isTemasysPluginUsed()) {
self.trace(
'onicecandidate',
JSON.stringify(event.candidate, null, ' '));
}
if (self.onicecandidate !== null) {
self.onicecandidate(event);
}
};
this.onaddstream = null;
this.peerconnection.onaddstream = function (event) {
self.trace('onaddstream', event.stream.id);
if (self.onaddstream !== null) {
self.onaddstream(event);
}
};
this.onremovestream = null;
this.peerconnection.onremovestream = function (event) {
self.trace('onremovestream', event.stream.id);
if (self.onremovestream !== null) {
self.onremovestream(event);
}
};
this.peerconnection.onaddstream = function (event) {
self._remoteStreamAdded(event.stream);
};
this.peerconnection.onremovestream = function (event) {
self._remoteStreamRemoved(event.stream);
};
this.onsignalingstatechange = null;
this.peerconnection.onsignalingstatechange = function (event) {
self.trace('onsignalingstatechange', self.signalingState);
if (self.onsignalingstatechange !== null) {
self.onsignalingstatechange(event);
}
};
this.oniceconnectionstatechange = null;
this.peerconnection.oniceconnectionstatechange = function (event) {
self.trace('oniceconnectionstatechange', self.iceConnectionState);
if (self.oniceconnectionstatechange !== null) {
self.oniceconnectionstatechange(event);
}
};
this.onnegotiationneeded = null;
this.peerconnection.onnegotiationneeded = function (event) {
self.trace('onnegotiationneeded');
if (self.onnegotiationneeded !== null) {
self.onnegotiationneeded(event);
}
};
self.ondatachannel = null;
this.peerconnection.ondatachannel = function (event) {
self.trace('ondatachannel', event);
if (self.ondatachannel !== null) {
self.ondatachannel(event);
}
};
// XXX: do all non-firefox browsers which we support also support this?
if (!RTCBrowserType.isFirefox() && this.maxstats) {
this.statsinterval = window.setInterval(function() {
self.peerconnection.getStats(function(stats) {
var results = stats.result();
var now = new Date();
for (var i = 0; i < results.length; ++i) {
results[i].names().forEach(function (name) {
var id = results[i].id + '-' + name;
if (!self.stats[id]) {
self.stats[id] = {
startTime: now,
endTime: now,
values: [],
times: []
};
}
self.stats[id].values.push(results[i].stat(name));
self.stats[id].times.push(now.getTime());
if (self.stats[id].values.length > self.maxstats) {
self.stats[id].values.shift();
self.stats[id].times.shift();
}
self.stats[id].endTime = now;
});
}
});
}, 1000);
}
}
/**
* Returns a string representation of a SessionDescription object.
*/
var dumpSDP = function(description) {
if (typeof description === 'undefined' || description == null) {
return '';
}
return 'type: ' + description.type + '\r\n' + description.sdp;
};
/**
* Called when new remote MediaStream is added to the PeerConnection.
* @param {MediaStream} stream the WebRTC MediaStream for remote participant
*/
TraceablePeerConnection.prototype._remoteStreamAdded = function (stream) {
if (!RTC.isUserStream(stream)) {
logger.info(
"Ignored remote 'stream added' event for non-user stream", stream);
return;
}
// Bind 'addtrack'/'removetrack' event handlers
if (RTCBrowserType.isChrome() || RTCBrowserType.isNWJS()
|| RTCBrowserType.isElectron()) {
stream.onaddtrack = (event) => {
this._remoteTrackAdded(event.target, event.track);
};
stream.onremovetrack = (event) => {
this._remoteTrackRemoved(event.target, event.track);
};
}
// Call remoteTrackAdded for each track in the stream
const streamAudioTracks = stream.getAudioTracks();
for (const audioTrack of streamAudioTracks) {
this._remoteTrackAdded(stream, audioTrack);
}
const streamVideoTracks = stream.getVideoTracks();
for (const videoTrack of streamVideoTracks) {
this._remoteTrackAdded(stream, videoTrack);
}
};
/**
* Called on "track added" and "stream added" PeerConnection events (because we
* handle streams on per track basis). Finds the owner and the SSRC for
* the track and passes that to ChatRoom for further processing.
* @param {MediaStream} stream the WebRTC MediaStream instance which is
* the parent of the track
* @param {MediaStreamTrack} track the WebRTC MediaStreamTrack added for remote
* participant
*/
TraceablePeerConnection.prototype._remoteTrackAdded = function (stream, track) {
const streamId = RTC.getStreamID(stream);
const mediaType = track.kind;
logger.info("Remote track added", streamId, mediaType);
// look up an associated JID for a stream id
if (!mediaType) {
GlobalOnErrorHandler.callErrorHandler(
new Error(
`MediaType undefined for remote track, stream id: ${streamId}`
));
// Abort
return;
}
const remoteSDP = new SDP(this.remoteDescription.sdp);
const mediaLines = remoteSDP.media.filter(
function (mediaLines){
return mediaLines.startsWith("m=" + mediaType);
});
if (!mediaLines.length) {
GlobalOnErrorHandler.callErrorHandler(
new Error(
"No media lines for type " + mediaType
+ " found in remote SDP for remote track: " + streamId));
// Abort
return;
}
let ssrcLines = SDPUtil.find_lines(mediaLines[0], 'a=ssrc:');
ssrcLines = ssrcLines.filter(
function (line) {
const msid
= RTCBrowserType.isTemasysPluginUsed() ? 'mslabel' : 'msid';
return line.indexOf(msid + ':' + streamId) !== -1;
});
if (!ssrcLines.length) {
GlobalOnErrorHandler.callErrorHandler(
new Error(
"No SSRC lines for streamId " + streamId
+ " for remote track, media type: " + mediaType));
// Abort
return;
}
// FIXME the length of ssrcLines[0] not verified, but it will fail
// with global error handler anyway
let trackSsrc = ssrcLines[0].substring(7).split(' ')[0];
const ownerEndpointId = this.signalingLayer.getSSRCOwner(trackSsrc);
if (!ownerEndpointId) {
GlobalOnErrorHandler.callErrorHandler(
new Error(
"No SSRC owner known for: " + trackSsrc
+ " for remote track, msid: " + streamId
+ " media type: " + mediaType));
// Abort
return;
}
logger.log('associated ssrc', ownerEndpointId, trackSsrc);
const peerMediaInfo
= this.signalingLayer.getPeerMediaInfo(ownerEndpointId, mediaType);
if (!peerMediaInfo) {
GlobalOnErrorHandler.callErrorHandler(
new Error("No peer media info available for: " + ownerEndpointId));
// Abort
return;
}
const muted = peerMediaInfo.muted;
const videoType = peerMediaInfo.videoType; // can be undefined
this.rtc._createRemoteTrack(
ownerEndpointId, stream, track, mediaType, videoType, trackSsrc, muted);
};
/**
* Handles remote stream removal.
* @param stream the WebRTC MediaStream object which is being removed from the
* PeerConnection
*/
TraceablePeerConnection.prototype._remoteStreamRemoved = function (stream) {
if (!RTC.isUserStream(stream)) {
const id = RTC.getStreamID(stream);
logger.info(
`Ignored remote 'stream removed' event for non-user stream ${id}`);
return;
}
// Call remoteTrackRemoved for each track in the stream
const streamVideoTracks = stream.getVideoTracks();
for (const videoTrack of streamVideoTracks) {
this._remoteTrackRemoved(stream, videoTrack);
}
const streamAudioTracks = stream.getAudioTracks();
for (const audioTrack of streamAudioTracks) {
this._remoteTrackRemoved(stream, audioTrack);
}
};
/**
* Handles remote media track removal.
* @param {MediaStream} stream WebRTC MediaStream instance which is the parent
* of the track.
* @param {MediaStreamTrack} track the WebRTC MediaStreamTrack which has been
* removed from the PeerConnection.
*/
TraceablePeerConnection.prototype._remoteTrackRemoved
= function (stream, track) {
const streamId = RTC.getStreamID(stream);
const trackId = track && track.id;
logger.info("Remote track removed", streamId, trackId);
if (!streamId) {
GlobalOnErrorHandler.callErrorHandler(
new Error("Remote track removal failed - no stream ID"));
// Abort
return;
}
if (!trackId) {
GlobalOnErrorHandler.callErrorHandler(
new Error("Remote track removal failed - no track ID"));
// Abort
return;
}
if (!this.rtc._removeRemoteTrack(streamId, trackId)) {
// NOTE this warning is always printed when user leaves the room,
// because we remove remote tracks manually on MUC member left event,
// before the SSRCs are removed by Jicofo. In most cases it is fine to
// ignore this warning, but still it's better to keep it printed for
// debugging purposes.
//
// We could change the behaviour to emit track removed only from here,
// but the order of the events will change and consuming apps could
// behave unexpectedly (the "user left" event would come before "track
// removed" events).
logger.warn(
`Removed track not found for msid: ${streamId},
track id: ${trackId}`);
}
};
/**
* @typedef {Object} SSRCGroupInfo
* @property {Array} ssrcs group's SSRCs
* @property {string} semantics
*/
/**
* @typedef {Object} TrackSSRCInfo
* @property {Array} ssrcs track's SSRCs
* @property {Array} groups track's SSRC groups
*/
/**
* Returns map with keys msid and TrackSSRCInfo values.
* @param {Object} desc the WebRTC SDP instance.
* @return {Map}
*/
function extractSSRCMap(desc) {
/**
* Track SSRC infos mapped by stream ID (msid)
* @type {Map}
*/
const ssrcMap = new Map();
/**
* Groups mapped by primary SSRC number
* @type {Map>}
*/
const groupsMap = new Map();
if (typeof desc !== 'object' || desc === null ||
typeof desc.sdp !== 'string') {
logger.warn('An empty description was passed as an argument.');
return ssrcMap;
}
const session = transform.parse(desc.sdp);
if (!Array.isArray(session.media)) {
return ssrcMap;
}
for (const mLine of session.media) {
if (!Array.isArray(mLine.ssrcs)) {
continue;
}
if (Array.isArray(mLine.ssrcGroups)) {
for (const group of mLine.ssrcGroups) {
if (typeof group.semantics !== 'undefined' &&
typeof group.ssrcs !== 'undefined') {
// Parse SSRCs and store as numbers
const groupSSRCs
= group.ssrcs.split(' ')
.map(ssrcStr => parseInt(ssrcStr));
const primarySSRC = groupSSRCs[0];
// Note that group.semantics is already present
group.ssrcs = groupSSRCs;
if (!groupsMap.has(primarySSRC)) {
groupsMap.set(primarySSRC, []);
}
groupsMap.get(primarySSRC).push(group);
}
}
}
for (const ssrc of mLine.ssrcs) {
if (ssrc.attribute !== 'msid') {
continue;
}
const msid = ssrc.value;
let ssrcInfo = ssrcMap.get(msid);
if (!ssrcInfo) {
ssrcInfo = {
ssrcs: [],
groups: []
};
ssrcMap.set(msid, ssrcInfo);
}
const ssrcNumber = ssrc.id;
ssrcInfo.ssrcs.push(ssrcNumber);
if (groupsMap.has(ssrcNumber)) {
const ssrcGroups = groupsMap.get(ssrcNumber);
for (const group of ssrcGroups) {
ssrcInfo.groups.push(group);
}
}
}
}
return ssrcMap;
}
/**
* Takes a SessionDescription object and returns a "normalized" version.
* Currently it only takes care of ordering the a=ssrc lines.
*/
var normalizePlanB = function(desc) {
if (typeof desc !== 'object' || desc === null ||
typeof desc.sdp !== 'string') {
logger.warn('An empty description was passed as an argument.');
return desc;
}
var transform = require('sdp-transform');
var session = transform.parse(desc.sdp);
if (typeof session !== 'undefined' &&
typeof session.media !== 'undefined' && Array.isArray(session.media)) {
session.media.forEach(function (mLine) {
// Chrome appears to be picky about the order in which a=ssrc lines
// are listed in an m-line when rtx is enabled (and thus there are
// a=ssrc-group lines with FID semantics). Specifically if we have
// "a=ssrc-group:FID S1 S2" and the "a=ssrc:S2" lines appear before
// the "a=ssrc:S1" lines, SRD fails.
// So, put SSRC which appear as the first SSRC in an FID ssrc-group
// first.
var firstSsrcs = [];
var newSsrcLines = [];
if (typeof mLine.ssrcGroups !== 'undefined' &&
Array.isArray(mLine.ssrcGroups)) {
mLine.ssrcGroups.forEach(function (group) {
if (typeof group.semantics !== 'undefined' &&
group.semantics === 'FID') {
if (typeof group.ssrcs !== 'undefined') {
firstSsrcs.push(Number(group.ssrcs.split(' ')[0]));
}
}
});
}
if (Array.isArray(mLine.ssrcs)) {
var i;
for (i = 0; i= 0) {
newSsrcLines.push(mLine.ssrcs[i]);
delete mLine.ssrcs[i];
}
}
for (i = 0; i groupInfo.semantics === "SIM");
if (simGroup) {
this.simulcast.setSsrcCache(simGroup.ssrcs);
}
const fidGroups
= ssrcInfo.groups.filter(
groupInfo => groupInfo.semantics === "FID");
if (fidGroups) {
const rtxSsrcMapping = new Map();
fidGroups.forEach(fidGroup => {
const primarySsrc = fidGroup.ssrcs[0];
const rtxSsrc = fidGroup.ssrcs[1];
rtxSsrcMapping.set(primarySsrc, rtxSsrc);
});
this.rtxModifier.setSsrcCache(rtxSsrcMapping);
}
}
};
TraceablePeerConnection.prototype.removeStream = function (stream) {
this.trace('removeStream', stream.id);
// FF doesn't support this yet.
if (this.peerconnection.removeStream) {
this.peerconnection.removeStream(stream);
}
};
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
this.trace('createDataChannel', label, opts);
return this.peerconnection.createDataChannel(label, opts);
};
TraceablePeerConnection.prototype.setLocalDescription
= function (description, successCallback, failureCallback) {
this.trace('setLocalDescription::preTransform', dumpSDP(description));
// if we're running on FF, transform to Plan A first.
if (RTCBrowserType.usesUnifiedPlan()) {
description = this.interop.toUnifiedPlan(description);
this.trace('setLocalDescription::postTransform (Plan A)',
dumpSDP(description));
}
var self = this;
this.peerconnection.setLocalDescription(description,
function () {
self.trace('setLocalDescriptionOnSuccess');
successCallback();
},
function (err) {
self.trace('setLocalDescriptionOnFailure', err);
self.eventEmitter.emit(
RTCEvents.SET_LOCAL_DESCRIPTION_FAILED,
err, self.peerconnection);
failureCallback(err);
}
);
};
TraceablePeerConnection.prototype.setRemoteDescription
= function (description, successCallback, failureCallback) {
this.trace('setRemoteDescription::preTransform', dumpSDP(description));
// TODO the focus should squeze or explode the remote simulcast
description = this.simulcast.mungeRemoteDescription(description);
this.trace(
'setRemoteDescription::postTransform (simulcast)',
dumpSDP(description));
if (this.options.preferH264) {
const parsedSdp = transform.parse(description.sdp);
const videoMLine = parsedSdp.media.find(m => m.type === "video");
SDPUtil.preferVideoCodec(videoMLine, "h264");
description.sdp = transform.write(parsedSdp);
}
// if we're running on FF, transform to Plan A first.
if (RTCBrowserType.usesUnifiedPlan()) {
description.sdp = this.rtxModifier.stripRtx(description.sdp);
this.trace('setRemoteDescription::postTransform (stripRtx)', dumpSDP(description));
description = this.interop.toUnifiedPlan(description);
this.trace(
'setRemoteDescription::postTransform (Plan A)',
dumpSDP(description));
}
if (RTCBrowserType.usesPlanB()) {
description = normalizePlanB(description);
}
var self = this;
this.peerconnection.setRemoteDescription(description,
function () {
self.trace('setRemoteDescriptionOnSuccess');
successCallback();
},
function (err) {
self.trace('setRemoteDescriptionOnFailure', err);
self.eventEmitter.emit(RTCEvents.SET_REMOTE_DESCRIPTION_FAILED,
err, self.peerconnection);
failureCallback(err);
}
);
/*
if (this.statsinterval === null && this.maxstats > 0) {
// start gathering stats
}
*/
};
/**
* Makes the underlying TraceablePeerConnection generate new SSRC for
* the recvonly video stream.
* @deprecated
*/
TraceablePeerConnection.prototype.generateRecvonlySsrc = function() {
// FIXME replace with SDPUtil.generateSsrc (when it's added)
const newSSRC = this.generateNewStreamSSRCInfo().ssrcs[0];
logger.info("Generated new recvonly SSRC: " + newSSRC);
this.sdpConsistency.setPrimarySsrc(newSSRC);
};
/**
* Makes the underlying TraceablePeerConnection forget the current primary video
* SSRC.
* @deprecated
*/
TraceablePeerConnection.prototype.clearRecvonlySsrc = function () {
logger.info("Clearing primary video SSRC!");
this.sdpConsistency.clearSsrcCache();
};
TraceablePeerConnection.prototype.close = function () {
this.trace('stop');
if (!this.rtc._removePeerConnection(this)) {
logger.error("RTC._removePeerConnection returned false");
}
if (this.statsinterval !== null) {
window.clearInterval(this.statsinterval);
this.statsinterval = null;
}
this.peerconnection.close();
};
/**
* Modifies the values of the setup attributes (defined by
* {@link http://tools.ietf.org/html/rfc4145#section-4}) of a specific SDP
* answer in order to overcome a delay of 1 second in the connection
* establishment between Chrome and Videobridge.
*
* @param {SDP} offer - the SDP offer to which the specified SDP answer is
* being prepared to respond
* @param {SDP} answer - the SDP to modify
* @private
*/
var _fixAnswerRFC4145Setup = function (offer, answer) {
if (!RTCBrowserType.isChrome()) {
// It looks like Firefox doesn't agree with the fix (at least in its
// current implementation) because it effectively remains active even
// after we tell it to become passive. Apart from Firefox which I tested
// after the fix was deployed, I tested Chrome only. In order to prevent
// issues with other browsers, limit the fix to Chrome for the time
// being.
return;
}
// XXX Videobridge is the (SDP) offerer and WebRTC (e.g. Chrome) is the
// answerer (as orchestrated by Jicofo). In accord with
// http://tools.ietf.org/html/rfc5245#section-5.2 and because both peers
// are ICE FULL agents, Videobridge will take on the controlling role and
// WebRTC will take on the controlled role. In accord with
// https://tools.ietf.org/html/rfc5763#section-5, Videobridge will use the
// setup attribute value of setup:actpass and WebRTC will be allowed to
// choose either the setup attribute value of setup:active or
// setup:passive. Chrome will by default choose setup:active because it is
// RECOMMENDED by the respective RFC since setup:passive adds additional
// latency. The case of setup:active allows WebRTC to send a DTLS
// ClientHello as soon as an ICE connectivity check of its succeeds.
// Unfortunately, Videobridge will be unable to respond immediately because
// may not have WebRTC's answer or may have not completed the ICE
// connectivity establishment. Even more unfortunate is that in the
// described scenario Chrome's DTLS implementation will insist on
// retransmitting its ClientHello after a second (the time is in accord
// with the respective RFC) and will thus cause the whole connection
// establishment to exceed at least 1 second. To work around Chrome's
// idiosyncracy, don't allow it to send a ClientHello i.e. change its
// default choice of setup:active to setup:passive.
if (offer && answer
&& offer.media && answer.media
&& offer.media.length == answer.media.length) {
answer.media.forEach(function (a, i) {
if (SDPUtil.find_line(
offer.media[i],
'a=setup:actpass',
offer.session)) {
answer.media[i]
= a.replace(/a=setup:active/g, 'a=setup:passive');
}
});
answer.raw = answer.session + answer.media.join('');
}
};
TraceablePeerConnection.prototype.createAnswer
= function (successCallback, failureCallback, constraints) {
this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
this.peerconnection.createAnswer(
(answer) => {
try {
this.trace(
'createAnswerOnSuccess::preTransform', dumpSDP(answer));
// if we're running on FF, transform to Plan A first.
if (RTCBrowserType.usesUnifiedPlan()) {
answer = this.interop.toPlanB(answer);
this.trace('createAnswerOnSuccess::postTransform (Plan B)',
dumpSDP(answer));
}
/**
* We don't keep ssrcs consitent for Firefox because rewriting
* the ssrcs between createAnswer and setLocalDescription
* breaks the caching in sdp-interop (sdp-interop must
* know about all ssrcs, and it updates its cache in
* toPlanB so if we rewrite them after that, when we
* try and go back to unified plan it will complain
* about unmapped ssrcs)
*/
if (!RTCBrowserType.isFirefox()) {
answer.sdp = this.sdpConsistency.makeVideoPrimarySsrcsConsistent(answer.sdp);
this.trace('createAnswerOnSuccess::postTransform (make primary video ssrcs consistent)',
dumpSDP(answer));
}
// Add simulcast streams if simulcast is enabled
if (!this.options.disableSimulcast
&& this.simulcast.isSupported()) {
answer = this.simulcast.mungeLocalDescription(answer);
this.trace(
'createAnswerOnSuccess::postTransform (simulcast)',
dumpSDP(answer));
}
if (!this.options.disableRtx && !RTCBrowserType.isFirefox()) {
answer.sdp = this.rtxModifier.modifyRtxSsrcs(answer.sdp);
this.trace(
'createAnswerOnSuccess::postTransform (rtx modifier)',
dumpSDP(answer));
}
// Fix the setup attribute (see _fixAnswerRFC4145Setup for
// details)
let remoteDescription = new SDP(this.remoteDescription.sdp);
let localDescription = new SDP(answer.sdp);
_fixAnswerRFC4145Setup(remoteDescription, localDescription);
answer.sdp = localDescription.raw;
this.eventEmitter.emit(RTCEvents.SENDRECV_STREAMS_CHANGED,
extractSSRCMap(answer));
successCallback(answer);
} catch (e) {
this.trace('createAnswerOnError', e);
this.trace('createAnswerOnError', dumpSDP(answer));
logger.error('createAnswerOnError', e, dumpSDP(answer));
failureCallback(e);
}
},
(err) => {
this.trace('createAnswerOnFailure', err);
this.eventEmitter.emit(RTCEvents.CREATE_ANSWER_FAILED, err,
this.peerconnection);
failureCallback(err);
},
constraints
);
};
TraceablePeerConnection.prototype.addIceCandidate
// eslint-disable-next-line no-unused-vars
= function (candidate, successCallback, failureCallback) {
//var self = this;
this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
this.peerconnection.addIceCandidate(candidate);
/* maybe later
this.peerconnection.addIceCandidate(candidate,
function () {
self.trace('addIceCandidateOnSuccess');
successCallback();
},
function (err) {
self.trace('addIceCandidateOnFailure', err);
failureCallback(err);
}
);
*/
};
TraceablePeerConnection.prototype.getStats = function(callback, errback) {
// TODO: Is this the correct way to handle Opera, Temasys?
if (RTCBrowserType.isFirefox()
|| RTCBrowserType.isTemasysPluginUsed()
|| RTCBrowserType.isReactNative()) {
// ignore for now...
if(!errback)
errback = function () {};
this.peerconnection.getStats(null, callback, errback);
} else {
this.peerconnection.getStats(callback);
}
};
/**
* Generate ssrc info object for a stream with the following properties:
* - ssrcs - Array of the ssrcs associated with the stream.
* - groups - Array of the groups associated with the stream.
*/
TraceablePeerConnection.prototype.generateNewStreamSSRCInfo = function () {
let ssrcInfo = {ssrcs: [], groups: []};
if (!this.options.disableSimulcast
&& this.simulcast.isSupported()) {
for (let i = 0; i < SIMULCAST_LAYERS; i++) {
ssrcInfo.ssrcs.push(SDPUtil.generateSsrc());
}
ssrcInfo.groups.push({
ssrcs: ssrcInfo.ssrcs.slice(),
semantics: "SIM"
});
} else {
ssrcInfo = {
ssrcs: [SDPUtil.generateSsrc()],
groups: []
};
}
if (!this.options.disableRtx && !RTCBrowserType.isFirefox()) {
// Specifically use a for loop here because we'll
// be adding to the list we're iterating over, so we
// only want to iterate through the items originally
// on the list
const currNumSsrcs = ssrcInfo.ssrcs.length;
for (let i = 0; i < currNumSsrcs; ++i) {
const primarySsrc = ssrcInfo.ssrcs[i];
const rtxSsrc = SDPUtil.generateSsrc();
ssrcInfo.ssrcs.push(rtxSsrc);
ssrcInfo.groups.push({
ssrcs: [primarySsrc, rtxSsrc],
semantics: "FID"
});
}
}
return ssrcInfo;
};
module.exports = TraceablePeerConnection;