diff --git a/openvidu-browser/src/OpenVidu/Connection.ts b/openvidu-browser/src/OpenVidu/Connection.ts index 87776d15..2002550b 100644 --- a/openvidu-browser/src/OpenVidu/Connection.ts +++ b/openvidu-browser/src/OpenVidu/Connection.ts @@ -17,6 +17,7 @@ import { Session } from './Session'; import { Stream } from './Stream'; +import { StreamLEGACY } from './StreamLEGACY'; import { ConnectionOptions } from '../OpenViduInternal/Interfaces/Private/ConnectionOptions'; import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions'; import { StreamOptionsServer } from '../OpenViduInternal/Interfaces/Private/StreamOptionsServer'; @@ -139,7 +140,11 @@ export class Connection { videoDimensions: !!opts.videoDimensions ? JSON.parse(opts.videoDimensions) : undefined, filter: !!opts.filter ? opts.filter : undefined }; - const stream = new Stream(this.session, streamOptions); + // TODO: CLEAN 2.15.0 LEGACY CODE + // THIS LINE: + const stream = this.session.openvidu.openviduServerVersion.startsWith('2.16') ? new Stream(this.session, streamOptions) : new StreamLEGACY(this.session, streamOptions); + // SHOULD GET BACK TO: + // const stream = new Stream(this.session, streamOptions); this.addStream(stream); }); diff --git a/openvidu-browser/src/OpenVidu/Publisher.ts b/openvidu-browser/src/OpenVidu/Publisher.ts index 60151ca6..4c8cabb4 100644 --- a/openvidu-browser/src/OpenVidu/Publisher.ts +++ b/openvidu-browser/src/OpenVidu/Publisher.ts @@ -18,6 +18,7 @@ import { OpenVidu } from './OpenVidu'; import { Session } from './Session'; import { Stream } from './Stream'; +import { StreamLEGACY } from './StreamLEGACY'; import { StreamManager } from './StreamManager'; import { EventDispatcher } from './EventDispatcher'; import { PublisherProperties } from '../OpenViduInternal/Interfaces/Public/PublisherProperties'; @@ -91,7 +92,13 @@ export class Publisher extends StreamManager { * @hidden */ constructor(targEl: string | HTMLElement, properties: PublisherProperties, openvidu: OpenVidu) { - super(new Stream((!!openvidu.session) ? openvidu.session : new Session(openvidu), { publisherProperties: properties, mediaConstraints: {} }), targEl); + + // TODO: CLEAN 2.15.0 LEGACY CODE + // THIS LINE: + super(openvidu.openviduServerVersion.startsWith('2.16') ? new Stream((!!openvidu.session) ? openvidu.session : new Session(openvidu), { publisherProperties: properties, mediaConstraints: {} }) : new StreamLEGACY((!!openvidu.session) ? openvidu.session : new Session(openvidu), { publisherProperties: properties, mediaConstraints: {} }), targEl); + // SHOULD GET BACK TO: + // super(new Stream((!!openvidu.session) ? openvidu.session : new Session(openvidu), { publisherProperties: properties, mediaConstraints: {} }), targEl); + this.properties = properties; this.openvidu = openvidu; diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index 8c3efe6f..49a14185 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -128,6 +128,19 @@ export class Session extends EventDispatcher { */ stopSpeakingEventsEnabledOnce = false; + + // TODO: CLEAN 2.15.0 LEGACY CODE + /** + * @hidden + */ + isFirstIonicIosSubscriber = true; + /** + * @hidden + */ + countDownForIonicIosSubscribersActive = true; + // END LEGACY CODE + + /** * @hidden */ @@ -716,6 +729,16 @@ export class Session extends EventDispatcher { streamEvent.callDefaultBehavior(); delete this.remoteStreamsCreated[stream.streamId]; + + + // TODO: CLEAN 2.15.0 LEGACY CODE + if (Object.keys(this.remoteStreamsCreated).length === 0) { + this.isFirstIonicIosSubscriber = true; + this.countDownForIonicIosSubscribersActive = true; + } + // END LEGACY CODE + + } delete this.remoteConnections[connection.connectionId]; this.ee.emitEvent('connectionDestroyed', [new ConnectionEvent(false, this, 'connectionDestroyed', connection, msg.reason)]); @@ -785,6 +808,16 @@ export class Session extends EventDispatcher { // Deleting the remote stream const streamId: string = connection.stream.streamId; delete this.remoteStreamsCreated[streamId]; + + + // TODO: CLEAN 2.15.0 LEGACY CODE + if (Object.keys(this.remoteStreamsCreated).length === 0) { + this.isFirstIonicIosSubscriber = true; + this.countDownForIonicIosSubscribersActive = true; + } + // END LEGACY CODE + + connection.removeStream(streamId); }) .catch(openViduError => { diff --git a/openvidu-browser/src/OpenVidu/StreamLEGACY.ts b/openvidu-browser/src/OpenVidu/StreamLEGACY.ts new file mode 100644 index 00000000..f0949665 --- /dev/null +++ b/openvidu-browser/src/OpenVidu/StreamLEGACY.ts @@ -0,0 +1,243 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Stream } from './Stream'; +import { Subscriber } from './Subscriber'; +import { WebRtcPeerLEGACY, WebRtcPeerSendrecvLEGACY, WebRtcPeerSendonlyLEGACY, WebRtcPeerRecvonlyLEGACY } from '../OpenViduInternal/WebRtcPeer/WebRtcPeerLEGACY'; +import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; + +/** + * @hidden + */ +import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; +/** + * @hidden + */ +const logger: OpenViduLogger = OpenViduLogger.getInstance(); + + + +/** + * Represents each one of the media streams available in OpenVidu Server for certain session. + * Each [[Publisher]] and [[Subscriber]] has an attribute of type Stream, as they give access + * to one of them (sending and receiving it, respectively) + */ +export class StreamLEGACY extends Stream { + + /** + * @hidden + */ + initWebRtcPeerSend(reconnect: boolean): Promise { + return new Promise((resolve, reject) => { + + if (!reconnect) { + this.initHarkEvents(); // Init hark events for the local stream + } + + const userMediaConstraints = { + audio: this.isSendAudio(), + video: this.isSendVideo() + }; + + const options = { + mediaStream: this.mediaStream, + mediaConstraints: userMediaConstraints, + onicecandidate: this.connection.sendIceCandidate.bind(this.connection), + iceServers: this.getIceServersConf(), + simulcast: false + }; + + const successCallback = (sdpOfferParam) => { + logger.debug('Sending SDP offer to publish as ' + + this.streamId, sdpOfferParam); + + const method = reconnect ? 'reconnectStream' : 'publishVideo'; + let params; + if (reconnect) { + params = { + stream: this.streamId + } + } else { + let typeOfVideo = ''; + if (this.isSendVideo()) { + typeOfVideo = (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) ? 'CUSTOM' : (this.isSendScreen() ? 'SCREEN' : 'CAMERA'); + } + params = { + doLoopback: this.displayMyRemote() || false, + hasAudio: this.isSendAudio(), + hasVideo: this.isSendVideo(), + audioActive: this.audioActive, + videoActive: this.videoActive, + typeOfVideo, + frameRate: !!this.frameRate ? this.frameRate : -1, + videoDimensions: JSON.stringify(this.videoDimensions), + filter: this.outboundStreamOpts.publisherProperties.filter + } + } + params['sdpOffer'] = sdpOfferParam; + + this.session.openvidu.sendRequest(method, params, (error, response) => { + if (error) { + if (error.code === 401) { + reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to publish")); + } else { + reject('Error on publishVideo: ' + JSON.stringify(error)); + } + } else { + (this.webRtcPeer).processAnswer(response.sdpAnswer, false) + .then(() => { + this.streamId = response.id; + this.creationTime = response.createdAt; + this.isLocalStreamPublished = true; + this.publishedOnce = true; + if (this.displayMyRemote()) { + this.localMediaStreamWhenSubscribedToRemote = this.mediaStream; + this.remotePeerSuccessfullyEstablished(); + } + if (reconnect) { + this.ee.emitEvent('stream-reconnected-by-publisher', []); + } else { + this.ee.emitEvent('stream-created-by-publisher', []); + } + this.initWebRtcStats(); + logger.info("'Publisher' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "published") + " to session"); + resolve(); + }) + .catch(error => { + reject(error); + }); + } + }); + }; + + if (reconnect) { + this.disposeWebRtcPeer(); + } + if (this.displayMyRemote()) { + this.webRtcPeer = new WebRtcPeerSendrecvLEGACY(options); + } else { + this.webRtcPeer = new WebRtcPeerSendonlyLEGACY(options); + } + this.webRtcPeer.addIceConnectionStateChangeListener('publisher of ' + this.connection.connectionId); + (this.webRtcPeer).generateOffer().then(sdpOffer => { + successCallback(sdpOffer); + }).catch(error => { + reject(new Error('(publish) SDP offer error: ' + JSON.stringify(error))); + }); + }); + } + + /** + * @hidden + */ + initWebRtcPeerReceive(reconnect: boolean): Promise { + return new Promise((resolve, reject) => { + + const offerConstraints = { + audio: this.inboundStreamOpts.hasAudio, + video: this.inboundStreamOpts.hasVideo + }; + logger.debug("'Session.subscribe(Stream)' called. Constraints of generate SDP offer", + offerConstraints); + const options = { + onicecandidate: this.connection.sendIceCandidate.bind(this.connection), + mediaConstraints: offerConstraints, + iceServers: this.getIceServersConf(), + simulcast: false + }; + + const successCallback = (sdpOfferParam) => { + logger.debug('Sending SDP offer to subscribe to ' + + this.streamId, sdpOfferParam); + + const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom'; + const params = { sdpOffer: sdpOfferParam }; + 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))); + } 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); + }); + } + }); + }; + + this.webRtcPeer = new WebRtcPeerRecvonlyLEGACY(options); + this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId); + (this.webRtcPeer).generateOffer() + .then(sdpOffer => { + successCallback(sdpOffer); + }) + .catch(error => { + reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error))); + }); + }); + } + + /** + * @hidden + */ + remotePeerSuccessfullyEstablished(): void { + this.mediaStream = new MediaStream(); + let receiver: RTCRtpReceiver; + for (receiver of this.webRtcPeer.pc.getReceivers()) { + if (!!receiver.track) { + this.mediaStream.addTrack(receiver.track); + } + } + logger.debug('Peer remote stream', this.mediaStream); + + if (!!this.mediaStream) { + + if (this.streamManager instanceof Subscriber) { + // Apply SubscriberProperties.subscribeToAudio and SubscriberProperties.subscribeToVideo + if (!!this.mediaStream.getAudioTracks()[0]) { + const enabled = !!((this.streamManager).properties.subscribeToAudio); + this.mediaStream.getAudioTracks()[0].enabled = enabled; + } + if (!!this.mediaStream.getVideoTracks()[0]) { + const enabled = !!((this.streamManager).properties.subscribeToVideo); + this.mediaStream.getVideoTracks()[0].enabled = enabled; + } + } + + this.updateMediaStreamInVideos(); + this.initHarkEvents(); // Init hark events for the remote stream + } + } + +} \ No newline at end of file diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeerLEGACY.ts b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeerLEGACY.ts new file mode 100644 index 00000000..cc04983f --- /dev/null +++ b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeerLEGACY.ts @@ -0,0 +1,174 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import platform = require('platform'); +import { OpenViduLogger } from '../Logger/OpenViduLogger'; +import { WebRtcPeerConfiguration } from './WebRtcPeer'; +import { WebRtcPeer } from './WebRtcPeer'; +/** + * @hidden + */ +const logger: OpenViduLogger = OpenViduLogger.getInstance(); + +export class WebRtcPeerLEGACY extends WebRtcPeer { + + constructor(protected configuration: WebRtcPeerConfiguration) { + super(configuration); + } + + /** + * 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) + */ + generateOffer(): Promise { + return new Promise((resolve, reject) => { + let offerAudio, offerVideo = true; + + // Constraints must have both blocks + 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: (this.configuration.mode !== 'sendonly' && offerAudio), + offerToReceiveVideo: (this.configuration.mode !== 'sendonly' && offerVideo) + }; + + 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() + .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'); + } + }) + .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'); + } + }) + .catch(error => reject(error)); + } + }); + } + + /** + * 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 + */ + processAnswer(sdpAnswer: string, needsTimeoutOnProcessAnswer: boolean): Promise { + return new Promise((resolve, reject) => { + const answer: RTCSessionDescriptionInit = { + type: 'answer', + sdp: sdpAnswer + }; + logger.debug('SDP answer received, setting remote description'); + + if (this.pc.signalingState === 'closed') { + reject('RTCPeerConnection is closed'); + } + + this.setRemoteDescriptionLEGACY(answer, needsTimeoutOnProcessAnswer, resolve, reject); + + }); + } + + /** + * @hidden + */ + private setRemoteDescriptionLEGACY(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 { + // Rest of platforms + this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error)); + } + } + +} + + +export class WebRtcPeerRecvonlyLEGACY extends WebRtcPeerLEGACY { + constructor(configuration: WebRtcPeerConfiguration) { + configuration.mode = 'recvonly'; + super(configuration); + } +} + +export class WebRtcPeerSendonlyLEGACY extends WebRtcPeerLEGACY { + constructor(configuration: WebRtcPeerConfiguration) { + configuration.mode = 'sendonly'; + super(configuration); + } +} + +export class WebRtcPeerSendrecvLEGACY extends WebRtcPeerLEGACY { + constructor(configuration: WebRtcPeerConfiguration) { + configuration.mode = 'sendrecv'; + super(configuration); + } +} \ No newline at end of file