UNPKG

nativescript-connectycube

Version:
884 lines (702 loc) 25.1 kB
const config = require('../cubeConfig'); const RTCPeerConnection = require('./cubeRTCPeerConnection'); const Utils = require('../cubeInternalUtils'); const Helpers = require('./cubeWebRTCHelpers'); const SignalingConstants = require('./cubeWebRTCConstants').SignalingConstants; const SessionState = require('./cubeWebRTCConstants').SessionState; const PeerConnectionState = require('./cubeWebRTCConstants').PeerConnectionState; const MediaDevicesImpl = require('../cubeDependencies').mediaDevices; class WebRTCSession { constructor(params) { this.ID = params.sessionID ? params.sessionID : generateUUID(); this.state = SessionState.NEW; this.initiatorID = parseInt(params.initiatorID); this.opponentsIDs = params.opIDs; this.callType = parseInt(params.callType); this.peerConnections = {}; this.localStream = null; this.mediaParams = null; this.signalingProvider = params.signalingProvider; this.currentUserID = params.currentUserID; this.maxBandwidth = params.maxBandwidth; this.answerTimer = null; this.startCallTime = 0; this.acceptCallTime = 0; this.onUserNotAnswerListener = null; this.onRemoteStreamListener = null; this.onSessionCloseListener = null; this.onCallStatsReport = null; this.onSessionConnectionStateChangedListener = null; } getDisplayMedia(params) { if (!MediaDevicesImpl.getDisplayMedia) { throw new Error("Your environment does not support 'getDisplayMedia' API"); } const elementId = params && params.elementId; const options = params && params.options; const mediaParams = { ...params }; this.mediaParams = mediaParams; delete mediaParams.elementId; delete mediaParams.options; return MediaDevicesImpl.getDisplayMedia(mediaParams) .then((stream) => { return this._upsertStream(stream, elementId, options); }) .catch((error) => { throw new Error(error); }); } getUserMedia(params) { const elementId = params && params.elementId; const options = params && params.options; const mediaParams = { ...params }; this.mediaParams = mediaParams; delete mediaParams.elementId; delete mediaParams.options; return MediaDevicesImpl.getUserMedia(mediaParams) .then((stream) => { return this._upsertStream(stream, elementId, options); }) .catch((error) => { throw new Error(error); }); } _upsertStream(stream, elementId, options) { const shouldUpdateCurrentStream = !!this.localStream; if (shouldUpdateCurrentStream) { this.localStream.getTracks().forEach((track) => { if (track.kind === 'audio' && stream.getAudioTracks().length === 0) { return; } else { track.stop(); this.localStream.removeTrack(track); } }); this._replaceTracks(stream); } else { this.localStream = stream; } if (elementId) { if (shouldUpdateCurrentStream) { this.detachMediaStream(elementId, options); } this.attachMediaStream(elementId, this.localStream, options); } return this.localStream; } _replaceTracks(stream) { const peers = this.peerConnections; stream.getTracks().forEach((track) => { if (track.kind === 'audio') { track.enabled = this.localStream.getAudioTracks().every((track) => track.enabled); } else { track.enabled = this.localStream.getVideoTracks().every((track) => track.enabled); } }); for (let userId in peers) { const peer = peers[userId]; peer.getSenders().map((sender) => { const track = stream.getTracks().find((track) => { return track.kind === sender.track.kind; }); if (track) { sender.replaceTrack(track); this.localStream.addTrack(track); } }); } } setMaxBandwidth(maxBandwidth) { const peers = this.peerConnections || []; if (peers.length < 1) { Helpers.trace("No 'RTCPeerConnection' to set 'maxBandwidth'"); return; } for (let userId in peers) { const peer = peers[userId]; peer.setRTCRtpSenderMaxBandwidth(maxBandwidth); } } connectionStateForUser(userID) { const peerConnection = this.peerConnections[userID]; if (peerConnection) { return peerConnection.state; } return null; } attachMediaStream(id, stream, options) { const elem = document.getElementById(id); if (elem) { if (typeof elem.srcObject === 'object') { elem.srcObject = stream; } else { elem.src = window.URL.createObjectURL(stream); } if (options && options.muted) { elem.muted = true; } if (options && options.mirror) { elem.style.webkitTransform = 'scaleX(-1)'; elem.style.transform = 'scaleX(-1)'; } elem.onloadedmetadata = function (e) { elem.play(); }; } else { throw new Error('Unable to attach media stream, element ' + id + ' is undefined'); } } detachMediaStream(id, options) { const elem = document.getElementById(id); if (elem) { elem.pause(); if (typeof elem.srcObject === 'object') { elem.srcObject = null; } else { elem.src = ''; } if (options && !options.mirror) { elem.style.webkitTransform = ''; elem.style.transform = ''; } } else { throw new Error('Unable to attach media stream, element ' + id + ' is undefined'); } } switchMediaTracks(deviceIds) { Utils.DLog('switchMediaTracks(deviceIds)', { deviceIds }); if (deviceIds && deviceIds.audio) { if (typeof this.mediaParams.audio === 'boolean') { this.mediaParams.audio = {}; } this.mediaParams.audio.deviceId = deviceIds.audio; } if (deviceIds && deviceIds.video) { if (typeof this.mediaParams.video === 'boolean') { this.mediaParams.video = {}; } this.mediaParams.video.deviceId = deviceIds.video; } this.localStream.getTracks().forEach((track) => { track.stop(); }); return MediaDevicesImpl.getUserMedia({ audio: this.mediaParams.audio || false, video: this.mediaParams.video || false, }) .then((stream) => { return this._replaceTracks(stream); }) .catch((error) => { throw new Error(error); }); } call(extension) { const ext = _prepareExtension(extension); Helpers.trace('Call, extension: ' + JSON.stringify(ext)); this.state = SessionState.ACTIVE; this.maxBandwidth = +ext.userInfo.maxBandwidth || 0; this.opponentsIDs.forEach((userID, i, arr) => { this._callInternal(userID, ext, true); }); } _callInternal(userID, extension, withOnNotAnswerCallback) { const peer = this._createPeer(userID, 'offer'); this.localStream.getTracks().forEach((track) => { peer.addTrack(track, this.localStream); }); this.peerConnections[userID] = peer; peer .getAndSetLocalSessionDescription(this.maxBandwidth) .then(() => { Helpers.trace('getAndSetLocalSessionDescription success'); /* let's send call requests to user */ peer._startDialingTimer(extension, withOnNotAnswerCallback); }) .catch((err) => { Helpers.trace('getAndSetLocalSessionDescription error: ' + err); }); } accept(extension) { const ext = _prepareExtension(extension); Helpers.trace('Accept, extension: ' + JSON.stringify(ext.userInfo)); if (this.state === SessionState.ACTIVE) { Helpers.traceError("Can't accept, the session is already active, return."); return; } if (this.state === SessionState.CLOSED) { Helpers.traceError("Can't accept, the session is already closed, return."); this.stop({}); return; } this.state = SessionState.ACTIVE; this.acceptCallTime = new Date(); this._clearAnswerTimer(); this._acceptInternal(this.initiatorID, ext); // group call const oppIDs = this._uniqueOpponentsIDsWithoutInitiator(); if (oppIDs.length > 0) { const offerTime = (this.acceptCallTime - this.startCallTime) / 1000; this._startWaitingOfferOrAnswerTimer(offerTime); oppIDs.forEach((opID) => { if (this.currentUserID > opID) { this._callInternal(opID, {}, true); } }); } } _acceptInternal(userID, extension) { const peerConnection = this.peerConnections[userID]; if (peerConnection) { this.localStream.getTracks().forEach((track) => { peerConnection.addTrack(track, this.localStream); }); peerConnection .setRemoteSessionDescription('offer', peerConnection.getRemoteSDP()) .then(() => { Helpers.trace("'setRemoteSessionDescription' success"); peerConnection .getAndSetLocalSessionDescription(this.maxBandwidth) .then(() => { Helpers.trace("'getAndSetLocalSessionDescription' success"); extension.sessionID = this.ID; extension.callType = this.callType; extension.callerID = this.initiatorID; extension.opponentsIDs = this.opponentsIDs; extension.sdp = peerConnection.localDescription.sdp; this.signalingProvider.sendMessage(userID, extension, SignalingConstants.SignalingType.ACCEPT); }) .catch((err) => { Helpers.trace("'getAndSetLocalSessionDescription' error: " + err); }); }) .catch((error) => { Helpers.traceError("'setRemoteSessionDescription' error: " + error); }); } else { Helpers.traceError("Can't accept the call, there is no information about peer connection by some reason."); } } reject(extension) { const ext = _prepareExtension(extension); Helpers.trace('Reject, extension: ' + JSON.stringify(ext.userInfo)); this.state = SessionState.REJECTED; this._clearAnswerTimer(); ext.sessionID = this.ID; ext.callType = this.callType; ext.callerID = this.initiatorID; ext.opponentsIDs = this.opponentsIDs; Object.keys(this.peerConnections).forEach((key) => { const peerConnection = this.peerConnections[key]; this.signalingProvider.sendMessage(peerConnection.userID, ext, SignalingConstants.SignalingType.REJECT); }); this._close(); } stop(extension) { const ext = _prepareExtension(extension); Helpers.trace('Stop, extension: ' + JSON.stringify(ext.userInfo)); this.state = SessionState.HUNGUP; if (this.answerTimer) { this._clearAnswerTimer(); } ext.sessionID = this.ID; ext.callType = this.callType; ext.callerID = this.initiatorID; ext.opponentsIDs = this.opponentsIDs; Object.keys(this.peerConnections).forEach((key) => { const peerConnection = this.peerConnections[key]; this.signalingProvider.sendMessage(peerConnection.userID, ext, SignalingConstants.SignalingType.STOP); }); this._close(); } canInitiateIceRestart(userID) { const peerConnection = this.peerConnections[userID]; return peerConnection.type === 'offer'; } iceRestart(userID) { const peerConnection = this.peerConnections[userID]; peerConnection .getAndSetLocalSessionDescription(this.maxBandwidth, { iceRestart: true }) .then((offer) => { Helpers.trace('[iceRestart][getAndSetLocalSessionDescription] Ok'); const ext = { sessionID: this.ID, sdp: offer.sdp, }; this.signalingProvider.sendMessage(userID, ext, SignalingConstants.SignalingType.RESTART); }) .catch((err) => { Helpers.trace('[iceRestart][getAndSetLocalSessionDescription] Error: ' + err); }); } mute(type) { this._muteStream(0, type); } unmute(type) { this._muteStream(1, type); } _processOnCall(callerID, extension) { const oppIDs = this._uniqueOpponentsIDs(); oppIDs.forEach((opID) => { const pConn = this.peerConnections[opID]; if (pConn) { if (opID == callerID) { pConn.updateRemoteSDP(extension.sdp); /** The group call logic starts here */ if (callerID != this.initiatorID && this.state === SessionState.ACTIVE) { this._acceptInternal(callerID, {}); } } } else { /** create peer connections for each opponent */ let peerConnection; if (opID != callerID && this.currentUserID > opID) { peerConnection = this._createPeer(opID, 'offer'); } else { peerConnection = this._createPeer(opID, 'answer'); } this.peerConnections[opID] = peerConnection; if (opID == callerID) { peerConnection.updateRemoteSDP(extension.sdp); this._startAnswerTimer(); } } }); } _processOnAccept(userID, extension) { const peerConnection = this.peerConnections[userID]; if (peerConnection) { peerConnection._clearDialingTimer(); peerConnection .setRemoteSessionDescription('answer', extension.sdp) .then(() => { Helpers.trace("'setRemoteSessionDescription' success"); }) .catch((error) => { Helpers.traceError("'setRemoteSessionDescription' error: " + error); }); } else { Helpers.traceWarning("Ignore 'OnAccept', there is no information about peer connection by some reason."); } } _processOnReject(userID, extension) { const peerConnection = this.peerConnections[userID]; this._clearWaitingOfferOrAnswerTimer(); if (peerConnection) { peerConnection.release(); } else { Helpers.traceWarning("Ignore 'OnReject', there is no information about peer connection by some reason."); } this._closeSessionIfAllConnectionsClosed(); } _processOnStop(userID, extension) { this._clearAnswerTimer(); /** drop the call if the initiator did it */ if (userID === this.initiatorID) { const pcKeys = Object.keys(this.peerConnections); if (pcKeys.length > 0) { pcKeys.forEach((key) => { this.peerConnections[key].release(); }); } else { Helpers.traceWarning("Ignore 'OnStop', there is no information about peer connections by some reason."); } } else { const pc = this.peerConnections[userID]; if (pc) { pc.release(); } else { Helpers.traceWarning("Ignore 'OnStop', there is no information about peer connection by some reason."); } } this._closeSessionIfAllConnectionsClosed(); } _processOnIceCandidates(userID, extension) { const peerConnection = this.peerConnections[userID]; if (peerConnection) { peerConnection.addCandidates(extension.iceCandidates); } else { Helpers.traceWarning("Ignore 'OnIceCandidates', there is no information about peer connection by some reason."); } } _processOnIceRestart(userID, extension) { const peerConnection = this.peerConnections[userID]; if (peerConnection) { peerConnection .setRemoteSessionDescription('offer', extension.sdp) .then(() => { Helpers.trace("[_processOnIceRestart]'setRemoteSessionDescription' success"); peerConnection .getAndSetLocalSessionDescription(this.maxBandwidth) .then((answer) => { Helpers.trace("[_processOnIceRestart]'getAndSetLocalSessionDescription' success"); const ext = { sessionID: this.ID, sdp: answer.sdp, }; this.signalingProvider.sendMessage(userID, ext, SignalingConstants.SignalingType.RESTART_ACCEPT); }) .catch((err) => { Helpers.trace("[_processOnIceRestart] 'getAndSetLocalSessionDescription' error: " + err); }); }) .catch((error) => { Helpers.traceError("[_processOnIceRestart] 'setRemoteSessionDescription' error: " + error); }); } else { Helpers.traceWarning( "[_processOnIceRestart] Ignore 'OnIceRestart', there is no information about peer connection by some reason." ); } } _processOnIceRestartAccept(userID, extension) { const peerConnection = this.peerConnections[userID]; if (peerConnection) { peerConnection .setRemoteSessionDescription('answer', extension.sdp) .then(() => { Helpers.trace("[_processOnIceRestartAccept] 'setRemoteSessionDescription' success"); }) .catch((error) => { Helpers.traceError("[_processOnIceRestartAccept] 'setRemoteSessionDescription' error: " + error); }); } else { Helpers.traceWarning( "[_processOnIceRestartAccept] Ignore 'OnIceRestartAccept', there is no information about peer connection by some reason." ); } } _processCall(peerConnection, ext) { const extension = ext || {}; extension.sessionID = this.ID; extension.callType = this.callType; extension.callerID = this.initiatorID; extension.opponentsIDs = this.opponentsIDs; extension.sdp = peerConnection.localDescription.sdp; extension.userInfo = ext.userInfo || {}; this.signalingProvider.sendMessage(peerConnection.userID, extension, SignalingConstants.SignalingType.CALL); } _processIceCandidates(peerConnection, iceCandidates) { const extension = {}; extension.sessionID = this.ID; extension.callType = this.callType; extension.callerID = this.initiatorID; extension.opponentsIDs = this.opponentsIDs; this.signalingProvider.sendCandidate(peerConnection.userID, iceCandidates, extension); } _processOnNotAnswer(peerConnection) { Helpers.trace('Answer timeout callback for session ' + this.ID + ' for user ' + peerConnection.userID); this._clearWaitingOfferOrAnswerTimer(); peerConnection.release(); Utils.safeCallbackCall(this.onUserNotAnswerListener, this, peerConnection.userID); this._closeSessionIfAllConnectionsClosed(); } _onRemoteStreamListener(userID, stream) { Utils.safeCallbackCall(this.onRemoteStreamListener, this, userID, stream); } _onCallStatsReport(userId, stats, error) { Utils.safeCallbackCall(this.onCallStatsReport, this, userId, stats, error); } _onSessionConnectionStateChangedListener(userID, connectionState) { Utils.safeCallbackCall(this.onSessionConnectionStateChangedListener, this, userID, connectionState); } _createPeer(userID, peerConnectionType) { this.startCallTime = new Date(); const pcConfig = { iceServers: _prepareIceServers(config.videochat.iceServers), }; if (config.videochat.alwaysRelayCalls) { pcConfig.iceTransportPolicy = 'relay'; } Helpers.trace('_createPeer, iceServers: ' + JSON.stringify(pcConfig)); const peer = new RTCPeerConnection(pcConfig); peer._init(this, userID, this.ID, peerConnectionType); return peer; } _close() { Object.keys(this.peerConnections).forEach((key) => { const peer = this.peerConnections[key]; try { peer.release(); } catch (e) { Utils.DLog('Peer close error:', e); } }); this._closeLocalMediaStream(); this.state = SessionState.CLOSED; Utils.safeCallbackCall(this.onSessionCloseListener, this); } _closeSessionIfAllConnectionsClosed() { let isAllConnectionsClosed = true; Object.keys(this.peerConnections).forEach((key) => { const peerCon = this.peerConnections[key]; let peerState; try { /* * TODO: * Uses RTCPeerConnection.signalingState instead RTCPeerConnection.iceConnectionState, * because state 'closed' comes after few time from Safari, but signaling state comes instantly */ peerState = peerCon.iceConnectionState === 'closed' ? 'closed' : peerCon.signalingState === 'closed' ? 'closed' : peerCon.released ? 'closed' : null; } catch (err) { Helpers.traceError(err); // need to set peerState to 'closed' on error. FF will crashed without this part. peerState = 'closed'; } if (peerState !== 'closed') { isAllConnectionsClosed = false; } }); Helpers.trace('All peer connections closed: ' + isAllConnectionsClosed); if (isAllConnectionsClosed) { this._closeLocalMediaStream(); Utils.safeCallbackCall(this.onSessionCloseListener, this); this.state = SessionState.CLOSED; } } _closeLocalMediaStream() { if (this.localStream) { this.localStream.getAudioTracks().forEach((audioTrack) => { audioTrack.stop(); }); this.localStream.getVideoTracks().forEach((videoTrack) => { videoTrack.stop(); }); this.localStream = null; } } _muteStream(enabled, type) { if (type === 'audio' && this.localStream.getAudioTracks().length > 0) { this.localStream.getAudioTracks().forEach((track) => { track.enabled = !!enabled; }); return; } if (type === 'video' && this.localStream.getVideoTracks().length > 0) { this.localStream.getVideoTracks().forEach((track) => { track.enabled = !!enabled; }); return; } } _clearAnswerTimer() { if (this.answerTimer) { Helpers.trace('_clearAnswerTimer'); clearTimeout(this.answerTimer); this.answerTimer = null; } } _startAnswerTimer() { Helpers.trace('_startAnswerTimer'); const answerTimeoutCallback = () => { Helpers.trace('_answerTimeoutCallback'); if (typeof this.onSessionCloseListener === 'function') { this._close(); } this.answerTimer = null; }; const answerTimeInterval = config.videochat.answerTimeInterval * 1000; this.answerTimer = setTimeout(answerTimeoutCallback, answerTimeInterval); } _clearWaitingOfferOrAnswerTimer() { if (this.waitingOfferOrAnswerTimer) { Helpers.trace('_clearWaitingOfferOrAnswerTimer'); clearTimeout(this.waitingOfferOrAnswerTimer); this.waitingOfferOrAnswerTimer = null; } } _startWaitingOfferOrAnswerTimer(time) { const timeout = config.videochat.answerTimeInterval - time < 0 ? 1 : config.videochat.answerTimeInterval - time; const waitingOfferOrAnswerTimeoutCallback = () => { Helpers.trace('waitingOfferOrAnswerTimeoutCallback'); Object.keys(this.peerConnections).forEach((key) => { const peerConnection = this.peerConnections[key]; if ( peerConnection.state === PeerConnectionState.CONNECTING || peerConnection.state === PeerConnectionState.NEW ) { this._processOnNotAnswer(peerConnection); } }); this.waitingOfferOrAnswerTimer = null; }; Helpers.trace('_startWaitingOfferOrAnswerTimer, timeout: ' + timeout); this.waitingOfferOrAnswerTimer = setTimeout(waitingOfferOrAnswerTimeoutCallback, timeout * 1000); } _uniqueOpponentsIDs() { const opponents = []; if (this.initiatorID !== this.currentUserID) { opponents.push(this.initiatorID); } this.opponentsIDs.forEach((userID) => { if (userID != this.currentUserID) { opponents.push(parseInt(userID)); } }); return opponents; } _uniqueOpponentsIDsWithoutInitiator() { const opponents = []; this.opponentsIDs.forEach((userID) => { if (userID != this.currentUserID) { opponents.push(parseInt(userID)); } }); return opponents; } toString() { return ( 'ID: ' + this.ID + ', initiatorID: ' + this.initiatorID + ', opponentsIDs: ' + this.opponentsIDs + ', state: ' + this.state + ', callType: ' + this.callType ); } } function generateUUID() { let d = new Date().getTime(); const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16); }); return uuid; } function _prepareExtension(extension = {}) { const ext = { userInfo: extension }; try { if (Utils.isObject(extension)) { ext.userInfo = Utils.cloneObject(extension, true); } else { Helpers.traceWarning("Ignore 'extension', must be an object."); } } catch (err) { Helpers.traceWarning(err.message); } return ext; } function _prepareIceServers(iceServers) { const iceServersCopy = Utils.cloneObject(iceServers); Object.keys(iceServersCopy).forEach((c, i, a) => { if (iceServersCopy[i].hasOwnProperty('url')) { iceServersCopy[i].urls = iceServersCopy[i].url; } else { iceServersCopy[i].url = iceServersCopy[i].urls; } }); return iceServersCopy; } module.exports = WebRTCSession;