nativescript-connectycube
Version:
ConnectyCube chat and video chat SDK for NativeScript
536 lines (440 loc) • 17.1 kB
JavaScript
const config = require('../cubeConfig');
const Helpers = require('./cubeWebRTCHelpers');
const SessionConnectionState = require('./cubeWebRTCConstants').SessionConnectionState;
const RTCPeerConnection = require('../cubeDependencies').RTCPeerConnection;
const RTCSessionDescription = require('../cubeDependencies').RTCSessionDescription;
const RTCIceCandidate = require('../cubeDependencies').RTCIceCandidate;
const MediaStream = require('../cubeDependencies').MediaStream;
const PeerConnectionState = require('./cubeWebRTCConstants').PeerConnectionState;
RTCPeerConnection.prototype._init = function (delegate, userID, sessionID, type) {
Helpers.trace('RTCPeerConnection init. userID: ' + userID + ', sessionID: ' + sessionID + ', type: ' + type);
this.delegate = delegate;
this.sessionID = sessionID;
this.userID = userID;
this.type = type;
this.remoteSDP = null;
this.state = PeerConnectionState.NEW;
this.remoteStream = new MediaStream();
this.ontrack = this.onAddRemoteMediaCallback.bind(this);
this.onicecandidate = this.onIceCandidateCallback.bind(this);
this.onsignalingstatechange = this.onSignalingStateCallback.bind(this);
this.oniceconnectionstatechange = this.onIceConnectionStateCallback.bind(this);
this.onStatusClosedChecker = undefined;
/** We use this timer interval to dial a user - produce the call requests each N seconds. */
this.dialingTimer = null;
this.answerTimeInterval = 0;
this.statsReportTimer = null;
this.iceCandidates = [];
this.released = false;
};
RTCPeerConnection.prototype.release = function () {
this._clearDialingTimer();
this._clearStatsReportTimer();
this.close();
// TODO: 'closed' state doesn't fires on Safari 11 (do it manually)
if (Helpers.getVersionSafari() >= 11) {
this.onIceConnectionStateCallback();
}
this.released = true;
};
RTCPeerConnection.prototype.updateRemoteSDP = function (newSDP) {
if (!newSDP) {
throw new Error("sdp string can't be empty.");
} else {
this.remoteSDP = newSDP;
}
};
RTCPeerConnection.prototype.getRemoteSDP = function () {
return this.remoteSDP;
};
RTCPeerConnection.prototype.setRemoteSessionDescription = function (type, remoteSDP) {
const desc = new RTCSessionDescription({
sdp: remoteSDP,
type: type,
});
return this.setRemoteDescription(desc);
};
RTCPeerConnection.prototype.getAndSetLocalSessionDescription = function (maxBandwidth, offerAnswerOptions = {}) {
return new Promise((resolve, reject) => {
this.state = PeerConnectionState.CONNECTING;
if (this.type === 'offer') {
this.createOffer(offerAnswerOptions)
.then((offer) => {
this.setLocalDescription(offer)
.then((result) => {
/* caller sets maxBandwidth to RTCRtpSender parameters' encodings */
this.setRTCRtpSenderMaxBandwidth(maxBandwidth);
resolve(offer);
})
.catch(reject);
})
.catch(reject);
} else if (!offerAnswerOptions.iceRestart) {
this.createAnswer(offerAnswerOptions)
.then((answer) => {
this.setLocalDescription(answer)
.then((result) => {
/* callee sets maxBandwidth (bitrate) to RTCRtpSender parameters' encodings */
this.setRTCRtpSenderMaxBandwidth(maxBandwidth);
resolve(answer);
})
.catch(reject);
})
.catch(reject);
} else {
throw new Error('iceRestart is possible only from offer side');
}
});
};
RTCPeerConnection.prototype.setRTCRtpSenderMaxBandwidth = function (maxBandwidth) {
const senders = this.getSenders() || [];
senders.forEach((sender) => {
if (sender.track.kind === 'video') {
const params = sender.getParameters();
if (!params.encodings) {
params.encodings = [];
}
if (!maxBandwidth) {
delete params.encodings[0]?.maxBitrate;
} else {
params.encodings[0].maxBitrate = maxBandwidth * 1000;
}
sender
.setParameters(params)
.then(() => {
Helpers.trace('Set maxBandwidth success [' + this.userID + ']: ' + maxBandwidth + ' kbps');
})
.catch((err) => {
Helpers.trace('Set maxBandwidth error [' + this.userID + ']: ' + err);
});
}
});
};
RTCPeerConnection.prototype.addCandidates = function (iceCandidates) {
for (let i = 0, len = iceCandidates.length; i < len; i++) {
const candidate = {
sdpMLineIndex: iceCandidates[i].sdpMLineIndex,
sdpMid: iceCandidates[i].sdpMid,
candidate: iceCandidates[i].candidate,
};
if (!candidate.candidate) {
continue;
}
this.addIceCandidate(
new RTCIceCandidate(candidate),
() => {},
(error) => {
Helpers.traceError("Error on 'addIceCandidate': " + error);
}
);
}
};
RTCPeerConnection.prototype.toString = function () {
return (
'sessionID: ' + this.sessionID + ', userID: ' + this.userID + ', type: ' + this.type + ', state: ' + this.state
);
};
/// CALLBACKS
RTCPeerConnection.prototype.onSignalingStateCallback = function () {
Helpers.trace('onSignalingStateCallback: ' + this.signalingState);
if (this.signalingState === 'stable' && this.iceCandidates.length > 0) {
this.delegate._processIceCandidates(this, this.iceCandidates);
this.iceCandidates.length = 0;
}
};
RTCPeerConnection.prototype.onIceCandidateCallback = function (event) {
const candidate = event.candidate;
if (candidate) {
const iceCandidateData = {
sdpMLineIndex: candidate.sdpMLineIndex,
sdpMid: candidate.sdpMid,
candidate: candidate.candidate,
};
if (this.signalingState === 'stable') {
this.delegate._processIceCandidates(this, [iceCandidateData]);
} else {
this.iceCandidates.push(iceCandidateData);
}
}
};
/** handler of remote media stream */
RTCPeerConnection.prototype.onAddRemoteMediaCallback = function (event) {
if (typeof this.delegate._onRemoteStreamListener === 'function') {
this.remoteStream.addTrack(event.track);
if (
(this.delegate.callType == 1 && this.remoteStream.getVideoTracks().length) ||
(this.delegate.callType == 2 && this.remoteStream.getAudioTracks().length)
) {
this.delegate._onRemoteStreamListener(this.userID, this.remoteStream);
}
this._getStatsWrap();
}
};
RTCPeerConnection.prototype.onIceConnectionStateCallback = function () {
Helpers.trace('onIceConnectionStateCallback: ' + this.iceConnectionState);
if (typeof this.delegate._onSessionConnectionStateChangedListener === 'function') {
let conState = null;
if (Helpers.getVersionSafari() >= 11) {
clearTimeout(this.onStatusClosedChecker);
}
switch (this.iceConnectionState) {
case 'checking':
this.state = PeerConnectionState.CHECKING;
conState = SessionConnectionState.CONNECTING;
break;
case 'connected':
this._clearWaitingReconnectTimer();
this.state = PeerConnectionState.CONNECTED;
conState = SessionConnectionState.CONNECTED;
break;
case 'completed':
this._clearWaitingReconnectTimer();
this.state = PeerConnectionState.COMPLETED;
conState = SessionConnectionState.COMPLETED;
break;
case 'failed':
this.state = PeerConnectionState.FAILED;
conState = SessionConnectionState.FAILED;
break;
case 'disconnected':
this._startWaitingReconnectTimer();
this.state = PeerConnectionState.DISCONNECTED;
conState = SessionConnectionState.DISCONNECTED;
// repeat to call onIceConnectionStateCallback to get status "closed"
if (Helpers.getVersionSafari() >= 11) {
this.onStatusClosedChecker = setTimeout(() => {
this.onIceConnectionStateCallback();
}, 500);
}
break;
// TODO: this state doesn't fires on Safari 11
case 'closed':
this._clearWaitingReconnectTimer();
this.state = PeerConnectionState.CLOSED;
conState = SessionConnectionState.CLOSED;
break;
default:
break;
}
if (conState) {
this.delegate._onSessionConnectionStateChangedListener(this.userID, conState);
}
}
};
/// PRIVATE
RTCPeerConnection.prototype._clearStatsReportTimer = function () {
if (this.statsReportTimer) {
clearInterval(this.statsReportTimer);
this.statsReportTimer = null;
}
};
RTCPeerConnection.prototype._getStatsWrap = function () {
let statsReportInterval;
let lastResult;
if (config.videochat && config.videochat.statsReportTimeInterval) {
if (isNaN(+config.videochat.statsReportTimeInterval)) {
Helpers.traceError('statsReportTimeInterval (' + config.videochat.statsReportTimeInterval + ') must be integer.');
return;
}
statsReportInterval = config.videochat.statsReportTimeInterval * 1000;
const _statsReportCallback = () => {
_getStats(
this,
lastResult,
(results, lastResults) => {
lastResult = lastResults;
this.delegate._onCallStatsReport(this.userID, results, null);
},
(err) => {
Helpers.traceError('_getStats error. ' + err.name + ': ' + err.message);
this.delegate._onCallStatsReport(this.userID, null, err);
}
);
};
Helpers.trace('Stats tracker has been started.');
this.statsReportTimer = setInterval(_statsReportCallback, statsReportInterval);
}
};
RTCPeerConnection.prototype._clearWaitingReconnectTimer = function () {
if (this.waitingReconnectTimeoutCallback) {
Helpers.trace('_clearWaitingReconnectTimer');
clearTimeout(this.waitingReconnectTimeoutCallback);
this.waitingReconnectTimeoutCallback = null;
}
};
RTCPeerConnection.prototype._startWaitingReconnectTimer = function () {
const timeout = config.videochat.disconnectTimeInterval * 1000;
const waitingReconnectTimeoutCallback = () => {
Helpers.trace('waitingReconnectTimeoutCallback');
clearTimeout(this.waitingReconnectTimeoutCallback);
this.release();
this.delegate._closeSessionIfAllConnectionsClosed();
};
Helpers.trace('_startWaitingReconnectTimer, timeout: ' + timeout);
this.waitingReconnectTimeoutCallback = setTimeout(waitingReconnectTimeoutCallback, timeout);
};
RTCPeerConnection.prototype._clearDialingTimer = function () {
if (this.dialingTimer) {
Helpers.trace('_clearDialingTimer');
clearInterval(this.dialingTimer);
this.dialingTimer = null;
this.answerTimeInterval = 0;
}
};
RTCPeerConnection.prototype._startDialingTimer = function (extension, withOnNotAnswerCallback) {
const dialingTimeInterval = config.videochat.dialingTimeInterval * 1000;
Helpers.trace('_startDialingTimer, dialingTimeInterval: ' + dialingTimeInterval);
const _dialingCallback = (extension, withOnNotAnswerCallback, skipIncrement) => {
if (!skipIncrement) {
this.answerTimeInterval += config.videochat.dialingTimeInterval * 1000;
}
Helpers.trace('_dialingCallback, answerTimeInterval: ' + this.answerTimeInterval);
if (this.answerTimeInterval >= config.videochat.answerTimeInterval * 1000) {
this._clearDialingTimer();
if (withOnNotAnswerCallback) {
this.delegate._processOnNotAnswer(this);
}
} else {
this.delegate._processCall(this, extension);
}
};
this.dialingTimer = setInterval(_dialingCallback, dialingTimeInterval, extension, withOnNotAnswerCallback, false);
// call for the 1st time
_dialingCallback(extension, withOnNotAnswerCallback, true);
};
/**
* PRIVATE
*/
function _getStats(peer, lastResults, successCallback, errorCallback) {
let statistic = {
local: {
audio: {},
video: {},
candidate: {},
},
remote: {
audio: {},
video: {},
candidate: {},
},
};
if (Helpers.getVersionFirefox()) {
let localStream = peer.getLocalStreams().length ? peer.getLocalStreams()[0] : peer.delegate.localStream,
localVideoSettings = localStream.getVideoTracks().length ? localStream.getVideoTracks()[0].getSettings() : null;
statistic.local.video.frameHeight = localVideoSettings && localVideoSettings.height;
statistic.local.video.frameWidth = localVideoSettings && localVideoSettings.width;
}
peer.getStats(null).then((results) => {
results.forEach((result) => {
let item;
if (result.bytesReceived && result.type === 'inbound-rtp') {
item = statistic.remote[result.mediaType];
item.bitrate = _getBitratePerSecond(result, lastResults, false);
item.bytesReceived = result.bytesReceived;
item.packetsReceived = result.packetsReceived;
item.timestamp = result.timestamp;
if (result.mediaType === 'video' && result.framerateMean) {
item.framesPerSecond = Math.round(result.framerateMean * 10) / 10;
}
} else if (result.bytesSent && result.type === 'outbound-rtp') {
item = statistic.local[result.mediaType];
item.bitrate = _getBitratePerSecond(result, lastResults, true);
item.bytesSent = result.bytesSent;
item.packetsSent = result.packetsSent;
item.timestamp = result.timestamp;
if (result.mediaType === 'video' && result.framerateMean) {
item.framesPerSecond = Math.round(result.framerateMean * 10) / 10;
}
} else if (result.type === 'local-candidate') {
item = statistic.local.candidate;
if (result.candidateType === 'host' && result.mozLocalTransport === 'udp' && result.transport === 'udp') {
item.protocol = result.transport;
item.ip = result.ipAddress;
item.port = result.portNumber;
} else if (!Helpers.getVersionFirefox()) {
item.protocol = result.protocol;
item.ip = result.ip;
item.port = result.port;
}
} else if (result.type === 'remote-candidate') {
item = statistic.remote.candidate;
item.protocol = result.protocol || result.transport;
item.ip = result.ip || result.ipAddress;
item.port = result.port || result.portNumber;
} else if (result.type === 'track' && result.kind === 'video' && !Helpers.getVersionFirefox()) {
if (result.remoteSource) {
item = statistic.remote.video;
item.frameHeight = result.frameHeight;
item.frameWidth = result.frameWidth;
item.framesPerSecond = _getFramesPerSecond(result, lastResults, false);
} else {
item = statistic.local.video;
item.frameHeight = result.frameHeight;
item.frameWidth = result.frameWidth;
item.framesPerSecond = _getFramesPerSecond(result, lastResults, true);
}
}
});
successCallback(statistic, results);
}, errorCallback);
const _getBitratePerSecond = (result, lastResults, isLocal) => {
let lastResult = lastResults && lastResults.get(result.id),
seconds = lastResult ? (result.timestamp - lastResult.timestamp) / 1000 : 5,
kilo = 1024,
bit = 8,
bitrate;
if (!lastResult) {
bitrate = 0;
} else if (isLocal) {
bitrate = (bit * (result.bytesSent - lastResult.bytesSent)) / (kilo * seconds);
} else {
bitrate = (bit * (result.bytesReceived - lastResult.bytesReceived)) / (kilo * seconds);
}
return Math.round(bitrate);
};
const _getFramesPerSecond = (result, lastResults, isLocal) => {
let lastResult = lastResults && lastResults.get(result.id),
seconds = lastResult ? (result.timestamp - lastResult.timestamp) / 1000 : 5,
framesPerSecond;
if (!lastResult) {
framesPerSecond = 0;
} else if (isLocal) {
framesPerSecond = (result.framesSent - lastResult.framesSent) / seconds;
} else {
framesPerSecond = (result.framesReceived - lastResult.framesReceived) / seconds;
}
return Math.round(framesPerSecond * 10) / 10;
};
}
function updateVideoBandwidthRestriction(sdp, maxBandwidth) {
if (!maxBandwidth || (typeof maxBandwidth === 'number' && maxBandwidth <= 0)) {
return sdp.replace(/b=AS:.*\r\n/, '').replace(/b=TIAS:.*\r\n/, '');
}
const isFirefox = !!Helpers.getVersionFirefox();
const modifier = isFirefox ? 'TIAS' : 'AS';
const bandwidth = isFirefox ? maxBandwidth * 1000 : maxBandwidth;
const lines = sdp.split('\n');
let line = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].indexOf('m=video') === 0) {
line = i;
break;
}
}
if (line === -1) {
return sdp;
}
line++;
while (lines[line].indexOf('i=') === 0 || lines[line].indexOf('c=') === 0) {
line++;
}
if (lines[line].indexOf('b') === 0) {
lines[line] = `b=${modifier}:${bandwidth}`;
return lines.join('\n');
}
let newLines = lines.slice(0, line);
newLines.push(`b=${modifier}:${bandwidth}`);
newLines = newLines.concat(lines.slice(line, lines.length));
return newLines.join('\n');
}
module.exports = RTCPeerConnection;