diff --git a/src/call/Call.js b/src/call/Call.js index 1e1f89d1b0aff72574a586bdf0cc052e3e231293..5c86d9a3a9cee1aebe84d4e14f8652b0b8824855 100644 --- a/src/call/Call.js +++ b/src/call/Call.js @@ -58,11 +58,8 @@ class Call { }; //webrtc - this._pc = null; - this._pendingOffer = null; - this._pendingOfferPriority = 0; - this._offerSequence = 0; - this._pendingRenegotiation = null; + // map connectionId - data (pc, pendingOffer, pendingOfferPriority, offerSequence, pendingRenegotiation, sequence, kaTimer) + this._pcData = {} // screen sharing this._remoteScreenSharer = null; @@ -70,7 +67,6 @@ class Call { //media connection this._connectionId = null; - this._sequence = 0; this._memberId = null; this._members = []; @@ -79,8 +75,6 @@ class Call { this._callMidParticipantMapping = {}; this._callParticipantsData = {}; - this._kaTimer = null; - this._kaInterval = 0; this._reconnectTimer = null; // for debugging purposes - for conf calls @@ -273,7 +267,7 @@ class Call { } set micMuted(muted) { - if (this._pc && this._callParticipantData && this._callParticipantData.mediaStream) { + if (this._connectionId && this._callParticipantData && this._callParticipantData.mediaStream) { if (this._callParticipantData.mediaStream.getAudioTracks().length > 0) { this._callParticipantData.mediaStream.getAudioTracks()[0].enabled = !muted; this._micMuted = muted; @@ -301,7 +295,7 @@ class Call { } if (this.isConference) { - if (!this._pc) { + if (!this._connectionId) { return; } this._sendConfStartStopMediaRequest(!!video, `${video ? REQUEST_ID_VIDEO_START : REQUEST_ID_VIDEO_STOP}`, MediaContentType.VIDEO); @@ -314,14 +308,14 @@ class Call { _setVideo(video) { console.log('[SDK] [temp] _setVideo', video); - if (this._pc && - (this._pc.iceConnectionState === 'completed' || this._pc.iceConnectionState === 'connected')) { + const pc = this.getPeerConnection(); + if (pc && (pc.iceConnectionState === 'completed' || pc.iceConnectionState === 'connected')) { console.log('[SDK] [temp] _setVideo 2'); this._video = video; if (video) { // add a track return navigator.mediaDevices.getUserMedia({video: true}).then(stream => { - if (!this._pc) { + if (!this._connectionId) { console.log('[SDK] set video - no PC'); return Promise.reject(); } @@ -381,7 +375,7 @@ class Call { } setScreenSharing = (start, skipRenegotiation, skipRequesting) => { - if (!this._pc) { + if (!this._connectionId) { return Promise.reject(); } if (this.isConference && this._pendingScreenSharingStream) { @@ -492,7 +486,7 @@ class Call { handshakePacket.addMessage(message); handshakePacket.setConnectionId(this._connectionId); - handshakePacket.setSequence(++this._sequence); + handshakePacket.setSequence(++this._pcData[this._connectionId].sequence); this._callingTransport.sendMediaControlPacket(handshakePacket, this._id, this._participantId).then(() => {}).catch(() => { if (start) { @@ -505,12 +499,15 @@ class Call { }); }; - _addMediaTrack(trackToAdd, stream) { - if (!this._pc.getTransceivers) { - this._pc.addTrack(trackToAdd, stream); - this._startRenegotiation(false); - } else { - const transceiver = this._pc.getTransceivers().find((transceiver) => { + _addMediaTrack = (trackToAdd, stream, pc2Use, skipRenegotiation) => { + const pc = pc2Use || this.getPeerConnection(); + if (pc && !pc.getTransceivers) { + pc.addTrack(trackToAdd, stream); + if (!skipRenegotiation) { + this._startRenegotiation(false); + } + } else if (pc) { + const transceiver = pc.getTransceivers().find((transceiver) => { if (this.isConference) { console.log('[sdk][temp] check track for conference', transceiver.mid, transceiver.direction, transceiver.currentDirection); if (!transceiver.currentDirection || transceiver.currentDirection === 'inactive') { @@ -536,15 +533,21 @@ class Call { } else { transceiver.direction = 'sendrecv'; } - this._startRenegotiation(false); + if (!skipRenegotiation) { + this._startRenegotiation(false); + } }, (e) => { console.warn('[SDK] add media to track', JSON.stringify(e)); - this._pc.addTrack(trackToAdd, stream); - this._startRenegotiation(false); + pc.addTrack(trackToAdd, stream); + if (!skipRenegotiation) { + this._startRenegotiation(false); + } }); } else { - this._pc.addTrack(trackToAdd, stream); - this._startRenegotiation(false); + pc.addTrack(trackToAdd, stream); + if (!skipRenegotiation) { + this._startRenegotiation(false); + } } } @@ -552,13 +555,14 @@ class Call { } _removeMediaTrack(trackToRemove) { + const pc = this.getPeerConnection(); let sender = null; - if (this._pc) { - sender = this._pc.getSenders().find(function (s) { + if (pc) { + sender = pc.getSenders().find(function (s) { return s.track === trackToRemove; }); if (sender) { - this._pc.removeTrack(sender); + pc.removeTrack(sender); try { if (sender.setTrack) { @@ -619,7 +623,7 @@ class Call { const packet = new CallSignaling.HandshakePacket(); packet.addMessage(message); packet.setConnectionId(this._connectionId); - packet.setSequence(++this._sequence); + packet.setSequence(++this._pcData[this._connectionId].sequence); this._callingTransport.sendMediaControlPacket(packet, this._id, this._participantId).then(() => {}).catch(() => {}); @@ -689,10 +693,11 @@ class Call { this._callingTransport.registerCallSignalingSse(this._id, this._participantId, (response) => { if (response.status === 200 && this._reconnectTimer) { - if (this._pc && (this._pc.iceConnectionState === 'disconnected' || this._pc.iceConnectionState === 'failed')) { + const pc = this.getPeerConnection(); + if (pc && (pc.iceConnectionState === 'disconnected' || pc.iceConnectionState === 'failed')) { this._startRenegotiation(true); - } else if (this._state === CallState.CALL_STATE_CONNECTING && this._pc - && (this._pc.iceConnectionState === 'connected' || this._pc.iceConnectionState === 'completed')) { + } else if (this._state === CallState.CALL_STATE_CONNECTING && pc + && (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed')) { this.turnConnectionTimer(false); this._state = CallState.CALL_STATE_CONNECTED; @@ -706,10 +711,11 @@ class Call { this.handleConfAudioPacket(response.data); } else if (response.status === ERROR_NETWORK || response.status === ERROR_NETWORK_FATAL) { console.warn("[SDK] EventSource failed (CallSignaling):", response.data); - if (this._kaTimer) { - clearTimeout(this._kaTimer); - this._kaTimer = 0; - } + Object.values(this._pcData).forEach(data => { + clearTimeout(data.kaTimer); + pcData.kaTimer = 0; + pcData.kaInterval = 0; + }); if (!this._reconnectTimer) { this._state = CallState.CALL_STATE_CONNECTING; this.turnConnectionTimer(true); @@ -732,16 +738,19 @@ class Call { packet.getMessageList().forEach(message => { console.log('[SDK] handleConfAudioPacket', message); const connectionId = packet.getConnectionId(); + if (!this._connectionId && connectionId) { + this._connectionId = connectionId; + } const clientRequestId = message.getClientRequestId(); const sequence = packet.getSequence(); if (message.hasOffer()) { - this._handleOfferObj(message.getOffer(), clientRequestId); + this._handleOfferObj(message.getOffer(), clientRequestId, connectionId); } else if (message.hasIceCandidate()) { - this._handleIceCandidate(message.getIceCandidate()); + this._handleIceCandidate(message.getIceCandidate(), connectionId); } else if (message.hasKeepAlive()) { - this._handleKeepAlive(message.getKeepAlive().getPeriodseconds()); + this._handleKeepAlive(message.getKeepAlive().getPeriodseconds(), connectionId); } else if (message.hasAnswer()) { - this._handleAnswerReceived(message.getAnswer().getContent(), message.getAnswer().getTrackInfoList()); + this._handleAnswerReceived(message.getAnswer().getContent(), message.getAnswer().getTrackInfoList(), connectionId); } else if (message.hasTerminate()) { this._handleTerminate(message.getTerminate().getReason()); } else if (message.hasConnectionOpened()) { @@ -753,37 +762,41 @@ class Call { } else if (message.hasResumeMedia()) { this._handlePauseMedia(false, message.getResumeMedia().getMediaList()); } else if (message.hasError()) { - this._handleHandshakeError(message.getError(), clientRequestId); + this._handleHandshakeError(message.getError(), clientRequestId, connectionId); } }); } - _handleOfferObj = (offerObj, clientRequestId) => { - if (!this.isConference && this._pendingOffer) { - if (this._pendingOfferPriority > offerObj.priority) { + _handleOfferObj = (offerObj, clientRequestId, connectionId) => { + this._initPcData(connectionId); + if (!this.isConference && this._pcData[connectionId].pendingOffer) { + if (this._pcData[connectionId].pendingOfferPriority > offerObj.priority) { console.log('[SDK] handle offer - lower priority'); return; } else { - this._pendingOffer = null; - this._pendingOfferPriority = 0; + this._pcData[connectionId].pendingOffer = null; + this._pcData[connectionId].pendingOfferPriority = 0; } } else { - this._pendingOffer = null; - this._pendingOfferPriority = 0; + this._pcData[connectionId].pendingOffer = null; + this._pcData[connectionId].pendingOfferPriority = 0; } - this._offerSequence = offerObj.getSequence(); + this._pcData[connectionId].offerSequence = offerObj.getSequence(); this._handleOffer(offerObj.getContent(), offerObj.getSsrcMappingList(), offerObj.getTrackInfoList(), - clientRequestId, offerObj.getMediaRequestList()); + clientRequestId, offerObj.getMediaRequestList(), connectionId); }; - _handleOffer(offerStr, ssrcMappingList, trackInfoList, clientRequestId, mediaRequestList) { + _handleOffer(offerStr, ssrcMappingList, trackInfoList, clientRequestId, mediaRequestList, connectionId) { if (!webrtcAdapter) { return; } - const renegotiation = this._pc != null; - this._createPeerConnection(); - console.log('[SDK] handleOffer:\n ' + offerStr, ssrcMappingList, trackInfoList, clientRequestId); + const renegotiation = !!this._initPcData(connectionId).pc; + const isNewClient = !renegotiation && !this.isConference && Object.keys(this._pcData).length > 1; + this._createPeerConnection(connectionId); + const pc = this.getPeerConnection(connectionId); + + console.log('[SDK] handleOffer:\n ' + offerStr, ssrcMappingList, trackInfoList, clientRequestId, connectionId); if (ssrcMappingList && ssrcMappingList.length) { this._initMembersStreamsMapping(offerStr, ssrcMappingList); } @@ -799,7 +812,7 @@ class Call { let promiseStart; if (!renegotiation && !this._mediaStream) { console.log('[SDK] [temp] handleOffer: getUserMedia will be called'); - promiseStart = navigator.mediaDevices.getUserMedia(self._mediaConstraints); + promiseStart = navigator.mediaDevices.getUserMedia(this._mediaConstraints); } else { console.log('[SDK] [temp] handleOffer: getUserMedia will NOT be called'); promiseStart = Promise.resolve(null); @@ -811,14 +824,19 @@ class Call { promiseStart .then(stream => { console.log('[SDK] got stream renegotiation:', renegotiation); - if (!renegotiation) { - this._handleLocalMediaStream(stream); + if (!renegotiation && !isNewClient) { + this._handleLocalMediaStream(stream, pc); } - console.log('[SDK] [temp] handleOffer: setRemoteDescription'); - return this._pc.setRemoteDescription(desc); + return pc.setRemoteDescription(desc); }, (e) => { console.log('[SDK] failed to get stream', e); - return this._pc.setRemoteDescription(desc); + return pc.setRemoteDescription(desc); + }) + .then(() => { + if (isNewClient) { + return this._setupNewClientPc(pc, true, connectionId); + } + return ''; }) .then(() => { console.log('[SDK] [temp] handleOffer: handling promiseMediaRequest', !!promiseMediaRequest); @@ -826,7 +844,7 @@ class Call { }) .then(() => { console.log('[SDK] [temp] handleOffer: creating answer'); - return self._pc.createAnswer() + return pc.createAnswer() }) .then((answer) => { console.log('[SDK] [temp] handleOffer: setting answer'); @@ -839,7 +857,7 @@ class Call { // answerToSend = this._fixSsSdpTrackId(answerToSend); // answer.sdp = answerToSend; // } - return self._pc.setLocalDescription(answer); + return pc.setLocalDescription(answer); }) .then(() => { console.log('[SDK] [temp] handleOffer: sending answer'); @@ -848,16 +866,16 @@ class Call { // console.log('[SDK] setLocalDescription - send', answerToSend); const answer = new Answer(); answer.setContent(answerToSend); - answer.setTrackInfoList(this._generateTrackInfoList(answerToSend)); + answer.setTrackInfoList(this._generateTrackInfoList(answerToSend, connectionId)); const message = new CallSignaling.HandshakeMessage(); message.setAnswer(answer); const packet = new CallSignaling.HandshakePacket(); packet.addMessage(message); - packet.setConnectionId(self._connectionId); - packet.setSequence(++this._sequence); + packet.setConnectionId(connectionId); + packet.setSequence(++this._pcData[connectionId].sequence); self._callingTransport.sendMediaControlPacket(packet, self._id, self._participantId).then(() => {}).catch(() => {}); }) - .catch((e) => self._handleGetUserMediaError.call(self, e)); + .catch((e) => this._handleGetUserMediaError(e, connectionId)); } _getMediaRequestPromise = (clientRequestId) => { @@ -916,15 +934,16 @@ class Call { console.log('[SDK] trackInfoList', message, ...toLog); } - _generateTrackInfoList = (sdp) => { + _generateTrackInfoList = (sdp, connectionId) => { const trackInfoList = []; - if (!this._pc) { + const pc = this.getPeerConnection(connectionId); + if (!pc) { return trackInfoList; } - if (!this._pc.getTransceivers) { + if (!pc.getTransceivers) { return this._generateTrackInfoListFromSdp(sdp); } - const transceivers = this._pc.getTransceivers(); + const transceivers = pc.getTransceivers(); transceivers.forEach(transceiver => { if (!transceiver.stopped && transceiver.sender && transceiver.sender.track && !transceiver.sender.track.muted) { const trackInfo = new TrackInfo(); @@ -988,7 +1007,7 @@ class Call { return trackInfoList; }; - _handleLocalMediaStream = (stream) => { + _handleLocalMediaStream = (stream, pc) => { if (!stream) { stream = this._mediaStream; } @@ -1017,7 +1036,7 @@ class Call { track.enabled = false; } - this._pc.addTrack(track, stream); + pc.addTrack(track, stream); } ); } else { @@ -1025,6 +1044,63 @@ class Call { } }; + _setupNewClientPc = (pc, promise, connectionId) => { + console.log('[SDK] handle new connection', !!pc, promise, connectionId); + if (!pc || !this._callParticipantData) { + console.warn('[SDK] handle new connection - no data'); + return; + } + let audioPromise; + let videoPromise; + let ssPromise; + + if (this._callParticipantData.mediaStream) { + const audioTrack = this._callParticipantData.mediaStream.getAudioTracks()[0]; + if (audioTrack) { + if (!promise) { + pc.addTrack(audioTrack, this._callParticipantData.mediaStream); + } else { + audioPromise = this._addMediaTrack(audioTrack, this._callParticipantData.mediaStream, pc, true); + } + } + const videoTrack = this._callParticipantData.mediaStream.getVideoTracks()[0]; + if (videoTrack) { + if (!promise) { + pc.addTrack(videoTrack, this._callParticipantData.mediaStream); + } else { + this._pcData[connectionId].pendingRenegotiation = false; + videoPromise = + this._addMediaTrack(videoTrack, this._callParticipantData.mediaStream, pc, true) + .then(() => { + console.log('[SDK] [temp] _setupNewClientPc addVideo'); + this._callParticipantData.mediaStream.addTrack(videoTrack); + this._callParticipantData.mediaElement.srcObject = null; + this._callParticipantData.mediaElement.srcObject = this._callParticipantData.mediaStream; + this._callParticipantData.reloadMedia(); + }); + } + } + } + if (this._callParticipantData.screenShareStream) { + const ssTrack = this._callParticipantData.screenShareStream.getVideoTracks()[0]; + if (ssTrack) { + if (!promise) { + pc.addTrack(ssTrack, this._callParticipantData.screenShareStream); + } else { + this._pcData[connectionId].pendingRenegotiation = false; + ssPromise = this._addMediaTrack(ssTrack, this._callParticipantData.screenShareStream, pc, true); + } + } + } + if (promise) { + audioPromise = audioPromise ? audioPromise : Promise.resolve(); + videoPromise = videoPromise ? videoPromise : Promise.resolve(); + ssPromise = ssPromise ? ssPromise : Promise.resolve(); + + return audioPromise.then(() => videoPromise).then(() => ssPromise); + } + }; + _setupCallParticipantData(callParticipantData, mediaElement, mediaStream, setActiveSpeaker) { if (mediaElement) { callParticipantData.mediaElement = mediaElement; @@ -1046,7 +1122,7 @@ class Call { _initParticipantsTrackInfo = (trackInfoList) => { const isPlanBInConf = this.isConference && !(isUnifiedPlanSupported() || this.forceUnifiedPlan); - if (!this._pc || !trackInfoList || isPlanBInConf) { + if (!this._connectionId || !trackInfoList || isPlanBInConf) { console.log('[SDK] initParticipantsTrackInf ignore', isPlanBInConf); return; } @@ -1137,26 +1213,29 @@ class Call { Object.values(callParticipantStreamMapping).forEach(callParticipantData => callParticipantData.cleanUp()); } - _handleGetUserMediaError = (e) => { - console.error('[SDK] error handling offer', e); + _handleGetUserMediaError = (e, connectionId) => { + console.error('[SDK] error handling offer', e, connectionId); // if (this._eventsListener && this._eventsListener.onerror()) + const pc = this.getPeerConnection(connectionId); + console.log('[SDK] error handling offer', pc && pc.connectionState, pc && pc.signalingState); - console.log('[SDK] error handling offer', this._pc.connectionState, this._pc.signalingState); - - if (this._pc.connectionState === 'completed' || this._pc.connectionState === 'connected') { + if (pc && (pc.connectionState === 'completed' || pc.connectionState === 'connected')) { this.callState = CallStates.CALL_STATE_CONNECTED; - this._pendingRenegotiation = false; + if (!connectionId) { + connectionId = this._connectionId; + } + this._pcData[connectionId].pendingRenegotiation = false; // if (this.isScreenSharing) { // this.setScreenSharing(false); // } - if (this._pendingOffer) { - this._pendingOffer = null; - this._pendingOfferPriority = null; + if (this._pcData[connectionId].pendingOffer) { + this._pcData[connectionId].pendingOffer = null; + this._pcData[connectionId].pendingOfferPriority = null; } - if (this._pc.signalingState !== 'stable') { - this._pc.setLocalDescription({type: 'rollback'}); - this._pc.setRemoteDescription({type: 'rollback'}); + if (pc.signalingState !== 'stable') { + pc.setLocalDescription({type: 'rollback'}); + pc.setRemoteDescription({type: 'rollback'}); } this._startRenegotiation(false); } else { @@ -1165,24 +1244,45 @@ class Call { } }; - _createPeerConnection = () => { - if (this._pc) { + _createPeerConnection = (connectionId) => { + if (this.getPeerConnection(connectionId)) { return; } const options = this.getPeerConnectionConfig(); console.log('[SDK] PeerConnection config:', options.iceServers.length, options.sdpSemantics); - this._pc = new RTCPeerConnection(options); + const pc = new RTCPeerConnection(options); - const self = this; - this._pc.onicecandidate = (e) => self._handleICECandidateEvent.call(self, e); - this._pc.oniceconnectionstatechange = (e) => self._handleICEConnectionStateChangeEvent.call(self, e); - // TODO do we need those ? - // this._pc.onicegatheringstatechange = (e) => self._handleICEGatheringStateChangeEvent.call(self, e); - this._pc.onsignalingstatechange = (e) => self._handleSignalingStateChangeEvent.call(self, e); - this._pc.onnegotiationneeded = (e) => self._handleNegotiationNeededEvent.call(self, e); - - this._pc.ontrack = (e) => self._handleTrackEvent.call(self, e); + pc.onicecandidate = (e) => this._handleICECandidateEvent(e, connectionId); + pc.oniceconnectionstatechange = (e) => this._handleICEConnectionStateChangeEvent(e, pc, connectionId); + // pc.onicegatheringstatechange = (e) => this._handleICEGatheringStateChangeEvent(e); + pc.onsignalingstatechange = (e) => this._handleSignalingStateChangeEvent(e, pc, connectionId); + pc.onnegotiationneeded = (e) => this._handleNegotiationNeededEvent(e, pc); + + pc.ontrack = (e) => this._handleTrackEvent(e, pc); + + this._initPcData(connectionId).pc = pc; + }; + _initPcData = (connectionId) => { + if (!this._pcData[connectionId]) { + this._pcData[connectionId] = { + pc: null, + pendingOffer: null, + pendingOfferPriority: 0, + offerSequence: 0, + pendingRenegotiation: null, + sequence: 0, + kaInterval: 0, + kaTimer: 0 + }; + } + return this._pcData[connectionId]; + } + + getPeerConnection = (connectionId) => { + const connId = connectionId ? connectionId : this._connectionId; + + return this._pcData[connId] ? this._pcData[connId].pc : null; }; getPeerConnectionConfig = () => { @@ -1195,7 +1295,7 @@ class Call { return { iceServers, sdpSemantics }; }; - _handleICECandidateEvent(event) { + _handleICECandidateEvent = (event, connectionId) => { if (event.candidate) { console.log("Outgoing ICE candidate: " + event.candidate.candidate); @@ -1205,34 +1305,35 @@ class Call { message.setIceCandidate(candidatePacket); const packet = new CallSignaling.HandshakePacket(); packet.addMessage(message); - packet.setConnectionId(this._connectionId); - packet.setSequence(++this._sequence); + packet.setConnectionId(connectionId); + packet.setSequence(++this._pcData[connectionId].sequence); this._callingTransport.sendMediaControlPacket(packet, this._id, this._participantId).then(() => {}).catch(() => {}); } } - _handleSignalingStateChangeEvent() { - console.log('[SDK] *** WebRTC signaling state changed to: ' + this._pc.signalingState); - switch(this._pc.signalingState) { + _handleSignalingStateChangeEvent(e, pc, connectionId) { + console.log('[SDK] *** WebRTC signaling state changed to: ' + pc.signalingState); + switch(pc.signalingState) { case 'closed': this.hangup(); break; case 'stable': if (this.callState === CallStates.CALL_STATE_ESTABLISHING && - (this._pc.iceConnectionState === 'connected' || this._pc.iceConnectionState === 'completed')) { + (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed')) { this.callState = CallStates.CALL_STATE_CONNECTED; } - if (this._pendingRenegotiation !== null) { - const iceRestart = this._pendingRenegotiation; - this._pendingRenegotiation = null; + if (this._pcData[connectionId].pendingRenegotiation !== null + && this.callState === CallStates.CALL_STATE_CONNECTED) { + const iceRestart = this._pcData[connectionId].pendingRenegotiation; + this._pcData[connectionId].pendingRenegotiation = null; this._startRenegotiation(iceRestart); } break; case 'have-remote-offer': - if (this._pc && this._pendingIceCandidates.length > 0) { + if (pc && this._pendingIceCandidates.length > 0) { console.log('[SDK] Adding pending ice candidates'); this._pendingIceCandidates.forEach(iceCandidate => { - this._pc.addIceCandidate(iceCandidate).catch((e) => console.log('[SDK] Failed to add pending ice candidate', e)); + pc.addIceCandidate(iceCandidate).catch((e) => console.log('[SDK] Failed to add pending ice candidate', e)); }); this._pendingIceCandidates = null; } @@ -1242,13 +1343,13 @@ class Call { } } - _handleICEConnectionStateChangeEvent(event) { - if (this._pc == null) { + _handleICEConnectionStateChangeEvent(event, pc, connectionId) { + if (this._connectionId == null) { return; } - console.log('[SDK] *** ICE connection state changed to ' + this._pc.iceConnectionState); + console.log('[SDK] *** ICE connection state changed to ' + pc.iceConnectionState); if (this._reconnectTimer) { - if (this._pc.iceConnectionState !== 'connected') { + if (pc.iceConnectionState !== 'connected') { console.log('[SDK] *** ICE connection state changed - IGNORED'); return; } else { @@ -1260,7 +1361,7 @@ class Call { } } - switch(this._pc.iceConnectionState) { + switch(pc.iceConnectionState) { case 'failed': const prevStatus = this.callState; this.callState = CallStates.CALL_STATE_FAILED; @@ -1269,8 +1370,8 @@ class Call { return; } - this._pendingOffer = null; - this._pendingOfferPriority = 0; + this._pcData[this._connectionId].pendingOffer = null; + this._pcData[this._connectionId].pendingOfferPriority = 0; this._startRenegotiation(true); break; @@ -1280,8 +1381,8 @@ class Call { break; case 'disconnected': this.callState = CallStates.CALL_STATE_DISCONNECTED; - // this._pendingOffer = null; - // this._pendingOfferPriority = 0; + // this._pcData[this._connectionId].pendingOffer = null; + // this._pcData[this._connectionId].pendingOfferPriority = 0; // // this.hangup(); // this._startRenegotiation(true); if (!this._reconnectTimer) { @@ -1293,14 +1394,19 @@ class Call { // falls trough case 'connected': this.callState = CallStates.CALL_STATE_CONNECTED; + if (this._pcData[connectionId].pendingRenegotiation !== null) { + this._startRenegotiation(!!this._pcData[connectionId].pendingRenegotiation); + this._pcData[connectionId].pendingRenegotiation = null; + } break; } } - _handleNegotiationNeededEvent(e) { - console.log("[SDK] *** WebRTC renegotiation needed", this._pc && this._pc.signalingState, !!this._pendingOffer); + _handleNegotiationNeededEvent(e, pc) { + console.log("[SDK] *** WebRTC renegotiation needed", pc && pc.signalingState, + !!this._pcData[this._connectionId].pendingOffer); - // if (!this._pc || this._pc.signalingState !== "stable") { + // if (!pc || pc.signalingState !== "stable") { // console.log("[SDK] *** WebRTC renegotiation needed: IGNORE"); // return; // } @@ -1308,22 +1414,23 @@ class Call { } _startRenegotiation = (iceRestart) => { - console.log("[SDK] renegotiation start", iceRestart, this._pc && this._pc.signalingState); - if (this._pc == null || this.isConference) { - console.log("[SDK] renegotiation start - return:", this._pc == null ? 'No PC' : 'Not allowed for conference calls'); + const pc = this.getPeerConnection(); + console.log("[SDK] renegotiation start", iceRestart, pc && pc.signalingState); + if (pc == null || this.isConference) { + console.log("[SDK] renegotiation start - return:", pc == null ? 'No PC' : 'Not allowed for conference calls'); return; } - if (this._pc.signalingState !== "stable" || this._pendingOffer) { - console.log("[SDK] renegotiation start: IGNORE 2", this._pc.signalingState); - this._pendingRenegotiation = iceRestart; + if (pc.signalingState !== "stable" || this._pcData[this._connectionId].pendingOffer) { + console.log("[SDK] renegotiation start: IGNORE 2", pc.signalingState); + this._pcData[this._connectionId].pendingRenegotiation = iceRestart; return; } // to prevent race conditions - this._pendingOffer = {}; - this._pendingOfferPriority = 0; + this._pcData[this._connectionId].pendingOffer = {} + this._pcData[this._connectionId].pendingOfferPriority = 0; - this._pc.createOffer({...this._offerOptions, iceRestart: !!iceRestart}).then((offer) => { + pc.createOffer({...this._offerOptions, iceRestart: !!iceRestart}).then((offer) => { if (this._state === CallStates.CALL_STATE_ESTABLISHING) { //already received the remove offer console.log("[SDK] renegotiation start - already establishing"); @@ -1331,49 +1438,46 @@ class Call { } console.log("[SDK] ---> Creating new description object to send to remote peer", offer.sdp); - this._pendingOffer = offer; - this._pendingOfferPriority = Math.floor(Math.random() * 0xFFFFFFFF); - this._offerSequence = ++this._offerSequence; + this._pcData[this._connectionId].pendingOffer = offer; + this._pcData[this._connectionId].pendingOfferPriority = Math.floor(Math.random() * 0xFFFFFFFF); + this._pcData[this._connectionId].offerSequence = + !this._pcData[this._connectionId].offerSequence ? 1 : this._pcData[this._connectionId].offerSequence + 1; const offerObj = new Offer(); offerObj.setContent(offer.sdp); - offerObj.setPriority(this._pendingOfferPriority); - offerObj.setSequence(this._offerSequence); - offerObj.setTrackInfoList(this._generateTrackInfoList(offer.sdp)); + offerObj.setPriority(this._pcData[this._connectionId].pendingOfferPriority); + offerObj.setSequence(this._pcData[this._connectionId].offerSequence); + offerObj.setTrackInfoList(this._generateTrackInfoList(offer.sdp, this._connectionId)); const message = new CallSignaling.HandshakeMessage(); message.setOffer(offerObj); const packet = new CallSignaling.HandshakePacket(); packet.addMessage(message); packet.setConnectionId(this._connectionId); - packet.setSequence(++this._sequence); + packet.setSequence(++this._pcData[this._connectionId].sequence); this._callingTransport.sendMediaControlPacket(packet, this._id, this._participantId).then(() => {}).catch(() => {}); }, (e) => { console.warn('Failed to create offer', e); - this._pendingOffer = null; + this._pcData[this._connectionId].pendingOffer = null; }); }; - hangup = () => { + hangup = (skipSendTerminate) => { console.log("[SDK] Closing the call", this._id); this._callingTransport.unregisterCallSignalingSse(this._id); this._pendingIceCandidates = null; + // Close the RTCPeerConnection - if (this._pc) { + if (this._connectionId) { console.log("[SDK] --> Closing the peer connection"); - - this._pc.getSenders().forEach(sender => this._stopMediaStreamTrack(sender.track)); - - // Disconnect all our event listeners; we don't want stray events - // to interfere with the hangup while it's ongoing. - this._pc.ontrack = null; - this._pc.onremovetrack = null; - this._pc.onicecandidate = null; - this._pc.oniceconnectionstatechange = null; - this._pc.onsignalingstatechange = null; - this._pc.onicegatheringstatechange = null; - this._pc.onnegotiationneeded = null; + this._connectionId = null; + Object.values(this._pcData).forEach(pcData => { + this._closePeerConnection(pcData.pc) + clearTimeout(pcData.kaTimer); + pcData.kaTimer = 0; + pcData.kaInterval = 0; + }); if (this._callParticipantsData) { Object.values(this._callParticipantsData).forEach(callData => callData.cleanUp()); @@ -1390,15 +1494,16 @@ class Call { this._pendingScreenSharingStream = null; } - // Close the peer connection - this._pc.close(); - this._pc = null; - this._pendingOffer = null; - this._pendingOfferPriority = 0; - this._offerSequence = 0; - this._remoteScreenSharer = null; + } else { + Object.values(this._pcData).forEach(data => { + clearTimeout(data.kaTimer); + pcData.kaTimer = 0; + pcData.kaInterval = 0; + }); } + this._pcData = {}; + if (this._mediaStream) { this._mediaStream.getTracks().forEach(track => this._stopMediaStreamTrack(track)); this._mediaStream = null; @@ -1408,17 +1513,14 @@ class Call { this._micMuted = false; this._video = false; this._speakerMuted = false; - if (this._kaTimer) { - clearTimeout(this._kaTimer); - this._kaTimer = 0; - this._kaInterval = 0; - } + this.turnConnectionTimer(false); - if (this._participantId) { + if (this._participantId && !skipSendTerminate) { + // TODO send terminate reason this._callingTransport.sendHangup(new Void(), this._id, this._participantId).then(() => this.participantId = null); - this.participantId = null; } + this.participantId = null; this._facade.notify(EventTypes.EVENT_CALL_UPDATE_HANGUP, this._id); if (this._eventsListener && (typeof this._eventsListener.onHangup === 'function')) { @@ -1426,8 +1528,33 @@ class Call { } }; - _handleIceCandidate(iceCandidateObj) { - if (!this._pc) { + _closePeerConnection(pc, skipStop) { + if (!pc) { + return; + } + + if (!skipStop) { + pc.getSenders().forEach(sender => this._stopMediaStreamTrack(sender.track)); + } + + // Disconnect all our event listeners; we don't want stray events + // to interfere with the hangup while it's ongoing. + pc.ontrack = null; + pc.onremovetrack = null; + pc.onicecandidate = null; + pc.oniceconnectionstatechange = null; + pc.onsignalingstatechange = null; + pc.onicegatheringstatechange = null; + pc.onnegotiationneeded = null; + + // Close the peer connection + pc.close(); + pc = null; + } + + _handleIceCandidate(iceCandidateObj, connectionId) { + const pc = this.getPeerConnection(connectionId); + if (!pc || !this._connectionId) { return; } const iceCandidate = new RTCIceCandidate({ @@ -1439,12 +1566,12 @@ class Call { this._pendingIceCandidates.push(iceCandidate); return; } - this._pc.addIceCandidate(iceCandidate).catch((e) => console.log('[SDK] Failed to add ice candidate', e)); + pc.addIceCandidate(iceCandidate).catch((e) => console.log('[SDK] Failed to add ice candidate', e)); } - _handleTrackEvent(event) { - console.log("[SDK] *** Track event", event, !!this._pc); - if (!this._pc) { + _handleTrackEvent(event, pc) { + console.log("[SDK] *** Track event", event, !!this._connectionId); + if (!this._connectionId) { return; } @@ -1456,15 +1583,15 @@ class Call { let mid = event.transceiver && event.transceiver.mid; if (!callParticipantData) { - if (!mid && this._pc.getTransceivers) { - const transceivers = this._pc.getTransceivers(); + if (!mid && pc.getTransceivers) { + const transceivers = pc.getTransceivers(); const transceiver = transceivers.find(transceiver => transceiver.receiver.track === track); if (transceiver) { mid = transceiver.mid; } } if (!mid) { - mid = this._getMidFromSdp(this._pc.remoteDescription.sdp, stream); + mid = this._getMidFromSdp(pc.remoteDescription.sdp, stream); } } callParticipantData = callParticipantData @@ -1543,8 +1670,8 @@ class Call { // Safari and FF workaround if (stream && this.isConference && (isUnifiedPlanSupported() || this.forceUnifiedPlan)) { stream.onremovetrack = (e) => { - if (this._pc && this._pc.getTransceivers) { - this._pc.getTransceivers().forEach(transceiver => { + if (pc && pc.getTransceivers) { + pc.getTransceivers().forEach(transceiver => { if (transceiver.mid === mid) { transceiver.stop(); } @@ -1641,7 +1768,7 @@ class Call { _notifyParticipantWithVideo(video, participantId) { console.log('[sdk][temp] _notifyParticipantWithVideo call', video, participantId); - if (!this._pc) { + if (!this._connectionId) { return; } @@ -1686,123 +1813,147 @@ class Call { return this._callParticipantsData[this._callStreamParticipantMapping[streamId]]; } - _handleKeepAlive(seconds) { - console.log("[SDK] [KA] Call handle KA:", seconds); + _handleKeepAlive(seconds, connectionId) { + console.log("[SDK] [KA] Call handle KA:", seconds, connectionId); if (seconds <= 0) { console.log("[SDK] [KA] Call handle KA: invalid interval", seconds); return; } - this._kaInterval = seconds; - if (this._kaTimer) { - console.log("[SDK] [KA] KA timer already running:", this._kaInterval); + const kaInterval = seconds; + this._initPcData(connectionId).kaInterval = kaInterval; + const kaTimer = this._initPcData(connectionId).kaTimer; + if (kaTimer) { + console.log("[SDK] [KA] KA timer already running:", kaInterval); return; } + const fn = () => { - console.log("[SDK] [KA] Send KA:", this._kaInterval); - if (!this._kaInterval) { + console.log("[SDK] Send KA:", seconds, connectionId); + if (!this._pcData[connectionId] || !this._pcData[connectionId].kaInterval) { console.log("[SDK] [KA] Send KA: STOPPED"); - return + return; } + const newKaInterval = this._pcData[connectionId].kaInterval; const ka = new CallSignaling.KeepAlive(); const keepAliveMessage = new CallSignaling.HandshakeMessage(); keepAliveMessage.setKeepAlive(ka); const keepAlivePacket = new CallSignaling.HandshakePacket(); keepAlivePacket.addMessage(keepAliveMessage); - keepAlivePacket.setConnectionId(this._connectionId); - keepAlivePacket.setSequence(++this._sequence); + keepAlivePacket.setConnectionId(connectionId); + keepAlivePacket.setSequence(++this._pcData[connectionId].sequence); this._callingTransport.sendMediaControlPacket(keepAlivePacket, this._id, this._participantId); - this._kaTimer = setTimeout(fn, this._kaInterval * 1000); - }; - this._kaTimer = setTimeout(fn, this._kaInterval * 1000); + this._initPcData(connectionId).kaTimer = setTimeout(fn, newKaInterval * 1000); + } + this._initPcData(connectionId).kaTimer = setTimeout(fn, kaInterval * 1000); } _handleTerminate(reason) { console.log('[SDK] Call Signaling Terminated', reason, this._participantId); - this.hangup(); + this.hangup(true); } _handleConnectionOpen(connectionOpened, connectionId, sequence) { console.log('[SDK] _handleConnectionOpen', connectionOpened, connectionId, sequence); + const isNewClient = this._connectionId && this._connectionId !== connectionId; this._connectionId = connectionId; - this._sequence = sequence; + this._initPcData(connectionId).sequence = sequence; if (connectionOpened.getSendOffer()) { //send offer if (!webrtcAdapter) { return; } - this._createPeerConnection(); - this.callState = this._mediaStream ? CallStates.CALL_STATE_ESTABLISHING : CallState.CALL_STATE_WAITING_PERMISSION; - const promise = this._mediaStream ? Promise.resolve(null) : navigator.mediaDevices.getUserMedia(this._mediaConstraints); + this._createPeerConnection(connectionId); + const pc = this.getPeerConnection(connectionId); + this.callState = this._mediaStream || isNewClient ? CallStates.CALL_STATE_ESTABLISHING : CallState.CALL_STATE_WAITING_PERMISSION; + const promise = this._mediaStream || isNewClient ? Promise.resolve(null) : navigator.mediaDevices.getUserMedia(this._mediaConstraints); let offerToSend = null; promise.then(stream => { - console.log('[SDK] got stream'); - this._handleLocalMediaStream(stream); - return this._pc.createOffer(this._offerOptions); + console.log('[SDK] got stream', isNewClient); + + if (isNewClient) { + this._setupNewClientPc(pc); + } else { + this._handleLocalMediaStream(stream, pc); + } + return pc.createOffer(this._offerOptions); }, (e) => { console.log('[SDK] failed to get stream', e); - return this._pc.createOffer(this._offerOptions); + return pc.createOffer(this._offerOptions); }) .then((offer) => { console.log("[SDK] ---> Creating new description object to send to remote peer 1"); offerToSend = offer.sdp; - return this._pc.setLocalDescription(offer); + return pc.setLocalDescription(offer); }) .then(() => { console.log("---> Sending offer to remote peer"); const offer = new Offer(); offer.setContent(offerToSend); - offer.setTrackInfoList(this._generateTrackInfoList(offerToSend)); + offer.setTrackInfoList(this._generateTrackInfoList(offerToSend, connectionId)); const message = new CallSignaling.HandshakeMessage(); message.setOffer(offer); const packet = new CallSignaling.HandshakePacket(); packet.addMessage(message); packet.setConnectionId(this._connectionId); - packet.setSequence(++this._sequence); + packet.setSequence(++this._pcData[connectionId].sequence); this._callingTransport.sendMediaControlPacket(packet, this._id, this._participantId).then(() => {}).catch(() => {}); }) .catch((e) => this._handleGetUserMediaError.call(this, e)); } } - _handleConnectionClosed(connectionId, sequence) { - this._sequence = 0; - this._connectionId = null; + _handleConnectionClosed = (connectionId, sequence) => { + console.log('[SDK] handleConnectionClosed', connectionId, sequence); + if (this._pcData[connectionId]) { + this._closePeerConnection(this._pcData[connectionId].pc, Object.keys(this._pcData).length > 1); + delete this._pcData[connectionId]; + } + if (this._connectionId === connectionId) { + this._connectionId = Object.keys(this._pcData)[0]; + } } - _handleAnswerReceived = (content, trackInfoList) => { - console.log('[SDK] handleAnswerReceived', content); - if (this._pendingOffer) { + _handleAnswerReceived = (content, trackInfoList, connectionId) => { + console.log('[SDK] handleAnswerReceived', content, connectionId); + const pc = this.getPeerConnection(connectionId); + if (!pc) { + console.warn('[SDK] handleAnswerReceived not expected - no PC'); + return; + } + + const pendingOffer = this._pcData[connectionId].pendingOffer; + if (pendingOffer) { console.log('[SDK] handleAnswerReceived - use the pending offer'); - this._pc.setLocalDescription(this._pendingOffer).then(() => { - if (this._pendingOffer) { + pc.setLocalDescription(pendingOffer).then(() => { + if (pendingOffer) { // Firefox workaround - if (!this.isOutgoing && content.indexOf('o=mozilla') >= 0 && this._pendingOffer.sdp.indexOf('a=setup:actpass') >= 0) { + if (!this.isOutgoing && content.indexOf('o=mozilla') >= 0 && pendingOffer.sdp.indexOf('a=setup:actpass') >= 0) { console.log('[SDK] handleAnswerReceived applying Firefox fix: a=setup:passive'); content = content.replace(/a=setup:active/gi, 'a=setup:passive'); } - this._pendingOffer = null; - this._pendingOfferPriority = null; + this._pcData[connectionId].pendingOffer = null; + this._pcData[connectionId].pendingOfferPriority = null; - this._handleAnswerReceived(content, trackInfoList); + this._handleAnswerReceived(content, trackInfoList, connectionId); } }) .catch(this._handleGetUserMediaError); return; } - if (this._pc.signalingState !== 'have-local-offer') { - console.log('[SDK] handleAnswerReceived not expected', this._pc.signalingState); + if (pc.signalingState !== 'have-local-offer') { + console.log('[SDK] handleAnswerReceived not expected', pc.signalingState); return; } const sdp = new RTCSessionDescription({sdp: content, type: 'answer'}); this._logTrackInfoList(trackInfoList, 'handleAnswerReceived'); this._initParticipantsTrackInfo(trackInfoList); - this._pc.setRemoteDescription(sdp).then(() => { + pc.setRemoteDescription(sdp).then(() => { // ignore }).catch(this._handleGetUserMediaError); }; @@ -1816,13 +1967,14 @@ class Call { }); }; - _handleHandshakeError = (handShakeError, clientRequestId) => { + _handleHandshakeError = (handShakeError, clientRequestId, connectionId) => { console.log('[SDK] _handleHandshakeError', handShakeError.getCode(), handShakeError.getMessage(), clientRequestId); if (clientRequestId && clientRequestId.startsWith(REQUEST_ID_SS_START) && this._pendingScreenSharingStream) { - if (this._pc.signalingState !== 'stable') { - this._pc.setLocalDescription({type: 'rollback'}); - this._pc.setRemoteDescription({type: 'rollback'}); + const pc = this.getPeerConnection(connectionId); + if (pc && pc.signalingState !== 'stable') { + pc.setLocalDescription({type: 'rollback'}); + pc.setRemoteDescription({type: 'rollback'}); } this.setScreenSharing(false, true);