react-native-enovawbrtc
Version:
Web Rtc
492 lines (404 loc) • 17.8 kB
JavaScript
const config = require('../Config');
const Helpers = require('./WebRTCHelpers');
const SessionConnectionState = require('./WebRTCConstants').SessionConnectionState
const RTCPeerConnection = require('../Dependencies').RTCPeerConnection;
const RTCSessionDescription = require('../Dependencies').RTCSessionDescription;
const RTCIceCandidate = require('../Dependencies').RTCIceCandidate;
const MediaStream = require('../Dependencies').MediaStream;
const PeerConnectionState = require('./WebRTCConstants').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.onicecandidate = this.onIceCandidateCallback.bind(this);
this.onsignalingstatechange = this.onSignalingStateCallback.bind(this);
this.oniceconnectionstatechange = this.onIceConnectionStateCallback.bind(this);
if (Helpers.getVersionSafari() >= 11) {
this.remoteStream = new MediaStream();
this.ontrack = this.onAddRemoteMediaCallback.bind(this);
this.onStatusClosedChecker = undefined;
} else {
this.remoteStream = null;
this.onaddstream = this.onAddRemoteMediaCallback.bind(this);
}
/** 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, remoteSessionDescription) {
const desc = new RTCSessionDescription({
sdp: setMediaBitrate(remoteSessionDescription, 'video', this.delegate.bandwidth),
type: type });
return this.setRemoteDescription(desc);
};
RTCPeerConnection.prototype.addLocalStream = function (localStream) {
if (localStream) {
this.addStream(localStream);
} else {
throw new Error("'RTCPeerConnection.addStream' error: stream is 'null'.");
}
};
RTCPeerConnection.prototype.getAndSetLocalSessionDescription = function (callType) {
return new Promise((resolve, reject) => {
this.state = PeerConnectionState.CONNECTING;
if (this.type === 'offer') {
this.createOffer()
.then(offer => {
offer.sdp = setMediaBitrate(offer.sdp, 'video', this.delegate.bandwidth);
this.setLocalDescription(offer).then(resolve).catch(reject);
}).catch(reject);
} else {
this.createAnswer()
.then(answer => {
answer.sdp = setMediaBitrate(answer.sdp, 'video', this.delegate.bandwidth);
this.setLocalDescription(answer).then(resolve).catch(reject);
}).catch(reject);
}
});
};
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
};
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') {
if (event.type === 'addstream') {
this.remoteStream = event.stream;
} else {
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 setMediaBitrate(sdp, media, bitrate) {
if (!bitrate) {
return sdp.replace(/b=AS:.*\r\n/, '').replace(/b=TIAS:.*\r\n/, '');
}
var lines = sdp.split('\n'),
line = -1,
modifier = Helpers.getVersionFirefox() ? 'TIAS' : 'AS',
amount = Helpers.getVersionFirefox() ? bitrate * 1024 : bitrate;
for (let i = 0; i < lines.length; i++) {
if (lines[i].indexOf("m=" + media) === 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 + ':' + amount;
return lines.join('\n');
}
let newLines = lines.slice(0, line);
newLines.push('b=' + modifier + ':' + amount);
newLines = newLines.concat(lines.slice(line, lines.length));
return newLines.join('\n');
}
module.exports = RTCPeerConnection;