diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index e4f3cfe5..b3386c5f 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -822,7 +822,7 @@ export class Stream extends EventDispatcher { simulcast: false }; - const successCallback = (sdpOfferParam) => { + const successOfferCallback = (sdpOfferParam) => { logger.debug('Sending SDP offer to publish as ' + this.streamId, sdpOfferParam); @@ -859,7 +859,7 @@ export class Stream extends EventDispatcher { reject('Error on publishVideo: ' + JSON.stringify(error)); } } else { - this.webRtcPeer.processAnswer(response.sdpAnswer, false) + this.webRtcPeer.processRemoteAnswer(response.sdpAnswer) .then(() => { this.streamId = response.id; this.creationTime = response.createdAt; @@ -894,10 +894,15 @@ export class Stream extends EventDispatcher { this.webRtcPeer = new WebRtcPeerSendonly(options); } this.webRtcPeer.addIceConnectionStateChangeListener('publisher of ' + this.connection.connectionId); - this.webRtcPeer.generateOffer().then(sdpOffer => { - successCallback(sdpOffer); + this.webRtcPeer.createOffer().then(sdpOffer => { + this.webRtcPeer.processLocalOffer(sdpOffer) + .then(() => { + successOfferCallback(sdpOffer.sdp); + }).catch(error => { + reject(new Error('(publish) SDP process local offer error: ' + JSON.stringify(error))); + }); }).catch(error => { - reject(new Error('(publish) SDP offer error: ' + JSON.stringify(error))); + reject(new Error('(publish) SDP create offer error: ' + JSON.stringify(error))); }); }); } @@ -906,6 +911,30 @@ export class Stream extends EventDispatcher { * @hidden */ initWebRtcPeerReceive(reconnect: boolean): Promise { + return new Promise((resolve, reject) => { + this.session.openvidu.sendRequest('prepareReceiveVideFrom', { sender: this.streamId }, (error, response) => { + if (error) { + reject(new Error('Error on prepareReceiveVideFrom: ' + JSON.stringify(error))); + } else { + this.completeWebRtcPeerReceive(response.sdpOffer, reconnect) + .then(() => { + logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed")); + this.remotePeerSuccessfullyEstablished(); + this.initWebRtcStats(); + resolve(); + }) + .catch(error => { + reject(error); + }); + } + }); + }); + } + + /** + * @hidden + */ + completeWebRtcPeerReceive(sdpOffer: string, reconnect: boolean): Promise { return new Promise((resolve, reject) => { const offerConstraints = { @@ -921,50 +950,60 @@ export class Stream extends EventDispatcher { simulcast: false }; - const successCallback = (sdpOfferParam) => { - logger.debug('Sending SDP offer to subscribe to ' - + this.streamId, sdpOfferParam); + const successAnswerCallback = (sdpAnswer) => { + logger.debug('Sending SDP answer to subscribe to ' + + this.streamId, sdpAnswer); const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom'; - const params = { sdpOffer: sdpOfferParam }; + const params = { sdpAnswer }; params[reconnect ? 'stream' : 'sender'] = this.streamId; this.session.openvidu.sendRequest(method, params, (error, response) => { if (error) { - reject(new Error('Error on recvVideoFrom: ' + JSON.stringify(error))); + reject(new Error('Error on receiveVideFrom: ' + JSON.stringify(error))); } else { - // Ios Ionic. Limitation: some bug in iosrtc cordova plugin makes it necessary - // to add a timeout before calling PeerConnection#setRemoteDescription during - // some time (400 ms) from the moment first subscriber stream is received - if (this.session.isFirstIonicIosSubscriber) { - this.session.isFirstIonicIosSubscriber = false; - setTimeout(() => { - // After 400 ms Ionic iOS subscribers won't need to run - // PeerConnection#setRemoteDescription after 250 ms timeout anymore - this.session.countDownForIonicIosSubscribersActive = false; - }, 400); - } - const needsTimeoutOnProcessAnswer = this.session.countDownForIonicIosSubscribersActive; - this.webRtcPeer.processAnswer(response.sdpAnswer, needsTimeoutOnProcessAnswer).then(() => { - logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed")); - this.remotePeerSuccessfullyEstablished(); - this.initWebRtcStats(); - resolve(); - }).catch(error => { - reject(error); - }); + resolve(); + // // Ios Ionic. Limitation: some bug in iosrtc cordova plugin makes it necessary + // // to add a timeout before calling PeerConnection#setRemoteDescription during + // // some time (400 ms) from the moment first subscriber stream is received + // if (this.session.isFirstIonicIosSubscriber) { + // this.session.isFirstIonicIosSubscriber = false; + // setTimeout(() => { + // // After 400 ms Ionic iOS subscribers won't need to run + // // PeerConnection#setRemoteDescription after 250 ms timeout anymore + // this.session.countDownForIonicIosSubscribersActive = false; + // }, 400); + // } + // const needsTimeoutOnProcessAnswer = this.session.countDownForIonicIosSubscribersActive; + // this.webRtcPeer.processAnswer(response.sdpAnswer, needsTimeoutOnProcessAnswer).then(() => { + // logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed")); + // this.remotePeerSuccessfullyEstablished(); + // this.initWebRtcStats(); + // resolve(); + // }).catch(error => { + // reject(error); + // }); } }); }; this.webRtcPeer = new WebRtcPeerRecvonly(options); this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId); - this.webRtcPeer.generateOffer() - .then(sdpOffer => { - successCallback(sdpOffer); + this.webRtcPeer.processRemoteOffer(sdpOffer) + .then(() => { + this.webRtcPeer.createAnswer().then(sdpAnswer => { + this.webRtcPeer.processLocalAnswer(sdpAnswer) + .then(() => { + successAnswerCallback(sdpAnswer.sdp); + }).catch(error => { + reject(new Error('(subscribe) SDP process local answer error: ' + JSON.stringify(error))); + }); + }).catch(error => { + reject(new Error('(subscribe) SDP create answer error: ' + JSON.stringify(error))); + }); }) .catch(error => { - reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error))); + reject(new Error('(subscribe) SDP process remote offer error: ' + JSON.stringify(error))); }); }); } diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts index 42e161ae..c4141973 100644 --- a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts +++ b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts @@ -117,10 +117,10 @@ export class WebRtcPeer { } /** - * Function that creates an offer, sets it as local description and returns the offer param - * to send to OpenVidu Server (will be the remote description of other peer) + * Creates an SDP offer from the local RTCPeerConnection to send to the other peer + * Only if the negotiation was initiated by the this peer */ - generateOffer(): Promise { + createOffer(): Promise { return new Promise((resolve, reject) => { let offerAudio, offerVideo = true; @@ -140,52 +140,32 @@ export class WebRtcPeer { logger.debug('RTCPeerConnection constraints: ' + JSON.stringify(constraints)); if (platform.name === 'Safari' && platform.ua!!.indexOf('Safari') !== -1) { + // Safari (excluding Ionic), at least on iOS just seems to support unified plan, whereas in other browsers is not yet ready and considered experimental if (offerAudio) { this.pc.addTransceiver('audio', { direction: this.configuration.mode, }); } - if (offerVideo) { this.pc.addTransceiver('video', { direction: this.configuration.mode, }); } - - this.pc - .createOffer() + this.pc.createOffer() .then(offer => { logger.debug('Created SDP offer'); - return this.pc.setLocalDescription(offer); - }) - .then(() => { - const localDescription = this.pc.localDescription; - - if (!!localDescription) { - logger.debug('Local description set', localDescription.sdp); - resolve(localDescription.sdp); - } else { - reject('Local description is not defined'); - } + resolve(offer); }) .catch(error => reject(error)); } else { // Rest of platforms - this.pc.createOffer(constraints).then(offer => { - logger.debug('Created SDP offer'); - return this.pc.setLocalDescription(offer); - }) - .then(() => { - const localDescription = this.pc.localDescription; - if (!!localDescription) { - logger.debug('Local description set', localDescription.sdp); - resolve(localDescription.sdp); - } else { - reject('Local description is not defined'); - } + this.pc.createOffer(constraints) + .then(offer => { + logger.debug('Created SDP offer'); + resolve(offer); }) .catch(error => reject(error)); } @@ -193,10 +173,95 @@ export class WebRtcPeer { } /** - * Function invoked when a SDP answer is received. Final step in SDP negotiation, the peer - * just needs to set the answer as its remote description + * Creates an SDP answer from the local RTCPeerConnection to send to the other peer + * Only if the negotiation was initiated by the other peer */ - processAnswer(sdpAnswer: string, needsTimeoutOnProcessAnswer: boolean): Promise { + createAnswer(): Promise { + return new Promise((resolve, reject) => { + let offerAudio, offerVideo = true; + if (!!this.configuration.mediaConstraints) { + offerAudio = (typeof this.configuration.mediaConstraints.audio === 'boolean') ? + this.configuration.mediaConstraints.audio : true; + offerVideo = (typeof this.configuration.mediaConstraints.video === 'boolean') ? + this.configuration.mediaConstraints.video : true; + } + const constraints: RTCOfferOptions = { + offerToReceiveAudio: offerAudio, + offerToReceiveVideo: offerVideo + }; + this.pc.createAnswer(constraints).then(sdpAnswer => { + resolve(sdpAnswer); + }).catch(error => { + reject(error); + }); + }); + } + + /** + * This peer initiated negotiation. Step 1/4 of SDP offer-answer protocol + */ + processLocalOffer(offer: RTCSessionDescriptionInit): Promise { + return new Promise((resolve, reject) => { + this.pc.setLocalDescription(offer) + .then(() => { + const localDescription = this.pc.localDescription; + if (!!localDescription) { + logger.debug('Local description set', localDescription.sdp); + resolve(); + } else { + reject('Local description is not defined'); + } + }) + .catch(error => { + reject(error); + }); + }); + } + + /** + * Other peer initiated negotiation. Step 2/4 of SDP offer-answer protocol + */ + processRemoteOffer(sdpOffer: string): Promise { + return new Promise((resolve, reject) => { + const offer: RTCSessionDescriptionInit = { + type: 'offer', + sdp: sdpOffer + }; + logger.debug('SDP offer received, setting remote description', offer); + + if (this.pc.signalingState === 'closed') { + reject('RTCPeerConnection is closed when trying to set remote description'); + } + // TODO: check if Ionic iOS still needs timeout on setting first remote description when subscribing + this.setRemoteDescription(offer, false) + .then(() => { + resolve(); + }) + .catch(error => { + reject(error); + }); + }); + } + + /** + * Other peer initiated negotiation. Step 3/4 of SDP offer-answer protocol + */ + processLocalAnswer(answer: RTCSessionDescriptionInit): Promise { + return new Promise((resolve, reject) => { + logger.debug('SDP answer created, setting local description'); + if (this.pc.signalingState === 'closed') { + reject('RTCPeerConnection is closed when trying to set local description'); + } + this.pc.setLocalDescription(answer) + .then(() => resolve()) + .catch(error => reject(error)); + }); + } + + /** + * This peer initiated negotiation. Step 4/4 of SDP offer-answer protocol + */ + processRemoteAnswer(sdpAnswer: string): Promise { return new Promise((resolve, reject) => { const answer: RTCSessionDescriptionInit = { type: 'answer', @@ -205,34 +270,33 @@ export class WebRtcPeer { logger.debug('SDP answer received, setting remote description'); if (this.pc.signalingState === 'closed') { - reject('RTCPeerConnection is closed'); + reject('RTCPeerConnection is closed when trying to set remote description'); } - - this.setRemoteDescription(answer, needsTimeoutOnProcessAnswer, resolve, reject); - + this.setRemoteDescription(answer, false) + .then(() => resolve()) + .catch(error => reject(error)); }); } /** * @hidden */ - setRemoteDescription(answer: RTCSessionDescriptionInit, needsTimeoutOnProcessAnswer: boolean, resolve: (value?: string | PromiseLike | undefined) => void, reject: (reason?: any) => void) { - if (platform['isIonicIos']) { - // Ionic iOS platform - if (needsTimeoutOnProcessAnswer) { - // 400 ms have not elapsed yet since first remote stream triggered Stream#initWebRtcPeerReceive - setTimeout(() => { - logger.info('setRemoteDescription run after timeout for Ionic iOS device'); - this.pc.setRemoteDescription(new RTCSessionDescription(answer)).then(() => resolve()).catch(error => reject(error)); - }, 250); - } else { - // 400 ms have elapsed - this.pc.setRemoteDescription(new RTCSessionDescription(answer)).then(() => resolve()).catch(error => reject(error)); - } - } else { + async setRemoteDescription(sdp: RTCSessionDescriptionInit, needsTimeoutOnProcessAnswer: boolean): Promise { + // if (platform['isIonicIos']) { + // // Ionic iOS platform + // if (needsTimeoutOnProcessAnswer) { + // // 400 ms have not elapsed yet since first remote stream triggered Stream#initWebRtcPeerReceive + // await new Promise(resolve => setTimeout(resolve, 250)); // Sleep for 250ms + // logger.info('setRemoteDescription run after timeout for Ionic iOS device'); + // return this.pc.setRemoteDescription(sdp); + // } else { + // // 400 ms have elapsed + // return this.pc.setRemoteDescription(sdp); + // } + // } else { // Rest of platforms - this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error)); - } + return this.pc.setRemoteDescription(sdp); + // } } /**