diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index a719044e..032d24e3 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -1,1209 +1,1192 @@ /* * (C) Copyright 2017-2019 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 { Connection } from './Connection'; import { Filter } from './Filter'; import { OpenVidu } from './OpenVidu'; import { Publisher } from './Publisher'; import { Stream } from './Stream'; import { StreamManager } from './StreamManager'; import { Subscriber } from './Subscriber'; import { Capabilities } from '../OpenViduInternal/Interfaces/Public/Capabilities'; import { EventDispatcher } from '../OpenViduInternal/Interfaces/Public/EventDispatcher'; import { SignalOptions } from '../OpenViduInternal/Interfaces/Public/SignalOptions'; import { SubscriberProperties } from '../OpenViduInternal/Interfaces/Public/SubscriberProperties'; import { ConnectionOptions } from '../OpenViduInternal/Interfaces/Private/ConnectionOptions'; import { ObjMap } from '../OpenViduInternal/Interfaces/Private/ObjMap'; import { SessionOptions } from '../OpenViduInternal/Interfaces/Private/SessionOptions'; import { ConnectionEvent } from '../OpenViduInternal/Events/ConnectionEvent'; import { FilterEvent } from '../OpenViduInternal/Events/FilterEvent'; import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpeakingEvent'; import { RecordingEvent } from '../OpenViduInternal/Events/RecordingEvent'; import { SessionDisconnectedEvent } from '../OpenViduInternal/Events/SessionDisconnectedEvent'; import { SignalEvent } from '../OpenViduInternal/Events/SignalEvent'; import { StreamEvent } from '../OpenViduInternal/Events/StreamEvent'; import { StreamPropertyChangedEvent } from '../OpenViduInternal/Events/StreamPropertyChangedEvent'; import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; import EventEmitter = require('wolfy87-eventemitter'); import platform = require('platform'); /** * Represents a video call. It can also be seen as a videoconference room where multiple users can connect. * Participants who publish their videos to a session can be seen by the rest of users connected to that specific session. * Initialized with [[OpenVidu.initSession]] method */ export class Session implements EventDispatcher { /** * Local connection to the Session. This object is defined only after [[Session.connect]] has been successfully executed, and can be retrieved subscribing to `connectionCreated` event */ connection: Connection; /** * Unique identifier of the Session */ sessionId: string; /** * Collection of all StreamManagers of this Session ([[Publisher]] and [[Subscriber]]) */ streamManagers: StreamManager[] = []; /** * Object defining the methods that the client is able to call. These are defined by the role of the token used to connect to the Session. * This object is only defined after [[Session.connect]] has been successfully resolved */ capabilities: Capabilities; // This map is only used to avoid race condition between 'joinRoom' response and 'onParticipantPublished' notification /** * @hidden */ remoteStreamsCreated: ObjMap = {}; - /** - * @hidden - */ - isFirstIonicIosSubscriber = true; - /** - * @hidden - */ - countDownForIonicIosSubscribers = true; - /** * @hidden */ remoteConnections: ObjMap = {}; /** * @hidden */ openvidu: OpenVidu; /** * @hidden */ options: SessionOptions; /** * @hidden */ speakingEventsEnabled = false; private ee = new EventEmitter(); /** * @hidden */ constructor(openvidu: OpenVidu) { this.openvidu = openvidu; } connect(token: string): Promise; connect(token: string, metadata: any): Promise; /** * Connects to the session using `token`. Parameter `metadata` allows you to pass extra data to share with other users when * they receive `streamCreated` event. The structure of `metadata` string is up to you (maybe some standardized format * as JSON or XML is a good idea). * * This metadata is not considered secure, as it is generated in the client side. To pass secure data, add it as a parameter in the * token generation operation (through the API REST, openvidu-java-client or openvidu-node-client). * * Only after the returned Promise is successfully resolved [[Session.connection]] object will be available and properly defined. * * #### Events dispatched * * The [[Session]] object of the local participant will first dispatch one or more `connectionCreated` events upon successful termination of this method: * - First one for your own local Connection object, so you can retrieve [[Session.connection]] property. * - Then one for each remote Connection previously connected to the Session, if any. Any other remote user connecting to the Session after you have * successfully connected will also dispatch a `connectionCreated` event when they do so. * * The [[Session]] object of the local participant will also dispatch a `streamCreated` event for each remote active [[Publisher]] that was already streaming * when connecting, just after dispatching all remote `connectionCreated` events. * * The [[Session]] object of every other participant connected to the session will dispatch a `connectionCreated` event. * * See [[ConnectionEvent]] and [[StreamEvent]] to learn more. * * @returns A Promise to which you must subscribe that is resolved if the the connection to the Session was successful and rejected with an Error object if not * */ connect(token: string, metadata?: any): Promise { return new Promise((resolve, reject) => { this.processToken(token); if (this.openvidu.checkSystemRequirements()) { // Early configuration to deactivate automatic subscription to streams this.options = { sessionId: this.sessionId, participantId: token, metadata: !!metadata ? this.stringClientMetadata(metadata) : '' }; this.connectAux(token).then(() => { resolve(); }).catch(error => { reject(error); }); } else { reject(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + platform.name + ' (version ' + platform.version + ') for ' + platform.os!!.family + ' is not supported in OpenVidu')); } }); } /** * Leaves the session, destroying all streams and deleting the user as a participant. * * #### Events dispatched * * The [[Session]] object of the local participant will dispatch a `sessionDisconnected` event. * This event will automatically unsubscribe the leaving participant from every Subscriber object of the session (this includes closing the WebRTCPeer connection and disposing all MediaStreamTracks) * and also deletes any HTML video element associated to each Subscriber (only those [created by OpenVidu Browser](/docs/how-do-i/manage-videos/#let-openvidu-take-care-of-the-video-players)). * For every video removed, each Subscriber object will dispatch a `videoElementDestroyed` event. * Call `event.preventDefault()` upon event `sessionDisconnected` to avoid this behavior and take care of disposing and cleaning all the Subscriber objects yourself. * See [[SessionDisconnectedEvent]] and [[VideoElementEvent]] to learn more to learn more. * * The [[Publisher]] object of the local participant will dispatch a `streamDestroyed` event if there is a [[Publisher]] object publishing to the session. * This event will automatically stop all media tracks and delete any HTML video element associated to it (only those [created by OpenVidu Browser](/docs/how-do-i/manage-videos/#let-openvidu-take-care-of-the-video-players)). * For every video removed, the Publisher object will dispatch a `videoElementDestroyed` event. * Call `event.preventDefault()` upon event `streamDestroyed` if you want to clean the Publisher object on your own or re-publish it in a different Session (to do so it is a mandatory requirement to call `Session.unpublish()` * or/and `Session.disconnect()` in the previous session). See [[StreamEvent]] and [[VideoElementEvent]] to learn more. * * The [[Session]] object of every other participant connected to the session will dispatch a `streamDestroyed` event if the disconnected participant was publishing. * This event will automatically unsubscribe the Subscriber object from the session (this includes closing the WebRTCPeer connection and disposing all MediaStreamTracks) * and also deletes any HTML video element associated to that Subscriber (only those [created by OpenVidu Browser](/docs/how-do-i/manage-videos/#let-openvidu-take-care-of-the-video-players)). * For every video removed, the Subscriber object will dispatch a `videoElementDestroyed` event. * Call `event.preventDefault()` upon event `streamDestroyed` to avoid this default behavior and take care of disposing and cleaning the Subscriber object yourself. * See [[StreamEvent]] and [[VideoElementEvent]] to learn more. * * The [[Session]] object of every other participant connected to the session will dispatch a `connectionDestroyed` event in any case. See [[ConnectionEvent]] to learn more. */ disconnect(): void { this.leave(false, 'disconnect'); } subscribe(stream: Stream, targetElement: string | HTMLElement): Subscriber; subscribe(stream: Stream, targetElement: string | HTMLElement, properties: SubscriberProperties): Subscriber; subscribe(stream: Stream, targetElement: string | HTMLElement, completionHandler: (error: Error | undefined) => void): Subscriber; subscribe(stream: Stream, targetElement: string | HTMLElement, properties: SubscriberProperties, completionHandler: (error: Error | undefined) => void): Subscriber; /** * Subscribes to a `stream`, adding a new HTML video element to DOM with `subscriberProperties` settings. This method is usually called in the callback of `streamCreated` event. * * #### Events dispatched * * The [[Subscriber]] object will dispatch a `videoElementCreated` event once the HTML video element has been added to DOM (only if you * [let OpenVidu take care of the video players](/docs/how-do-i/manage-videos/#let-openvidu-take-care-of-the-video-players)). See [[VideoElementEvent]] to learn more. * * The [[Subscriber]] object will dispatch a `streamPlaying` event once the remote stream starts playing. See [[StreamManagerEvent]] to learn more. * * @param stream Stream object to subscribe to * @param targetElement HTML DOM element (or its `id` attribute) in which the video element of the Subscriber will be inserted (see [[SubscriberProperties.insertMode]]). If *null* or *undefined* no default video will be created for this Subscriber. * You can always call method [[Subscriber.addVideoElement]] or [[Subscriber.createVideoElement]] to manage the video elements on your own (see [Manage video players](/docs/how-do-i/manage-videos) section) * @param completionHandler `error` parameter is null if `subscribe` succeeds, and is defined if it fails. */ subscribe(stream: Stream, targetElement: string | HTMLElement, param3?: ((error: Error | undefined) => void) | SubscriberProperties, param4?: ((error: Error | undefined) => void)): Subscriber { let properties: SubscriberProperties = {}; if (!!param3 && typeof param3 !== 'function') { properties = { insertMode: (typeof param3.insertMode !== 'undefined') ? ((typeof param3.insertMode === 'string') ? VideoInsertMode[param3.insertMode] : properties.insertMode) : VideoInsertMode.APPEND, subscribeToAudio: (typeof param3.subscribeToAudio !== 'undefined') ? param3.subscribeToAudio : true, subscribeToVideo: (typeof param3.subscribeToVideo !== 'undefined') ? param3.subscribeToVideo : true }; } else { properties = { insertMode: VideoInsertMode.APPEND, subscribeToAudio: true, subscribeToVideo: true }; } let completionHandler: (error: Error | undefined) => void; if (!!param3 && (typeof param3 === 'function')) { completionHandler = param3; } else if (!!param4) { completionHandler = param4; } console.info('Subscribing to ' + stream.connection.connectionId); stream.subscribe() .then(() => { console.info('Subscribed correctly to ' + stream.connection.connectionId); if (completionHandler !== undefined) { completionHandler(undefined); } }) .catch(error => { if (completionHandler !== undefined) { completionHandler(error); } }); const subscriber = new Subscriber(stream, targetElement, properties); if (!!subscriber.targetElement) { stream.streamManager.createVideoElement(subscriber.targetElement, properties.insertMode); } return subscriber; } /** * Promisified version of [[Session.subscribe]] */ subscribeAsync(stream: Stream, targetElement: string | HTMLElement): Promise; subscribeAsync(stream: Stream, targetElement: string | HTMLElement, properties: SubscriberProperties): Promise; subscribeAsync(stream: Stream, targetElement: string | HTMLElement, properties?: SubscriberProperties): Promise { return new Promise((resolve, reject) => { let subscriber: Subscriber; const callback = (error: Error) => { if (!!error) { reject(error); } else { resolve(subscriber); } }; if (!!properties) { subscriber = this.subscribe(stream, targetElement, properties, callback); } else { subscriber = this.subscribe(stream, targetElement, callback); } }); } /** * Unsubscribes from `subscriber`, automatically removing its associated HTML video elements. * * #### Events dispatched * * The [[Subscriber]] object will dispatch a `videoElementDestroyed` event for each video associated to it that was removed from DOM. * Only videos [created by OpenVidu Browser](/docs/how-do-i/manage-videos/#let-openvidu-take-care-of-the-video-players)) will be automatically removed * * See [[VideoElementEvent]] to learn more */ unsubscribe(subscriber: Subscriber): void { const connectionId = subscriber.stream.connection.connectionId; console.info('Unsubscribing from ' + connectionId); this.openvidu.sendRequest( 'unsubscribeFromVideo', { sender: subscriber.stream.connection.connectionId }, (error, response) => { if (error) { console.error('Error unsubscribing from ' + connectionId, error); } else { console.info('Unsubscribed correctly from ' + connectionId); } subscriber.stream.disposeWebRtcPeer(); subscriber.stream.disposeMediaStream(); } ); subscriber.stream.streamManager.removeAllVideos(); } /** * Publishes to the Session the Publisher object * * #### Events dispatched * * The local [[Publisher]] object will dispatch a `streamCreated` event upon successful termination of this method. See [[StreamEvent]] to learn more. * * The local [[Publisher]] object will dispatch a `streamPlaying` once the media stream starts playing. See [[StreamManagerEvent]] to learn more. * * The [[Session]] object of every other participant connected to the session will dispatch a `streamCreated` event so they can subscribe to it. See [[StreamEvent]] to learn more. * * @returns A Promise (to which you can optionally subscribe to) that is resolved only after the publisher was successfully published and rejected with an Error object if not */ publish(publisher: Publisher): Promise { return new Promise((resolve, reject) => { publisher.session = this; publisher.stream.session = this; if (!publisher.stream.publishedOnce) { // 'Session.unpublish(Publisher)' has NOT been called this.connection.addStream(publisher.stream); publisher.stream.publish() .then(() => { resolve(); }) .catch(error => { reject(error); }); } else { // 'Session.unpublish(Publisher)' has been called. Must initialize again Publisher publisher.initialize() .then(() => { this.connection.addStream(publisher.stream); publisher.reestablishStreamPlayingEvent(); publisher.stream.publish() .then(() => { resolve(); }) .catch(error => { reject(error); }); }).catch((error) => { reject(error); }); } }); } /** * Unpublishes from the Session the Publisher object. * * #### Events dispatched * * The [[Publisher]] object of the local participant will dispatch a `streamDestroyed` event. * This event will automatically stop all media tracks and delete any HTML video element associated to this Publisher * (only those videos [created by OpenVidu Browser](/docs/how-do-i/manage-videos/#let-openvidu-take-care-of-the-video-players)). * For every video removed, the Publisher object will dispatch a `videoElementDestroyed` event. * Call `event.preventDefault()` upon event `streamDestroyed` if you want to clean the Publisher object on your own or re-publish it in a different Session. * * The [[Session]] object of every other participant connected to the session will dispatch a `streamDestroyed` event. * This event will automatically unsubscribe the Subscriber object from the session (this includes closing the WebRTCPeer connection and disposing all MediaStreamTracks) and * delete any HTML video element associated to it (only those [created by OpenVidu Browser](/docs/how-do-i/manage-videos/#let-openvidu-take-care-of-the-video-players)). * For every video removed, the Subscriber object will dispatch a `videoElementDestroyed` event. * Call `event.preventDefault()` upon event `streamDestroyed` to avoid this default behavior and take care of disposing and cleaning the Subscriber object on your own. * * See [[StreamEvent]] and [[VideoElementEvent]] to learn more. */ unpublish(publisher: Publisher): void { const stream = publisher.stream; if (!stream.connection) { console.error('The associated Connection object of this Publisher is null', stream); return; } else if (stream.connection !== this.connection) { console.error('The associated Connection object of this Publisher is not your local Connection.' + "Only moderators can force unpublish on remote Streams via 'forceUnpublish' method", stream); return; } else { console.info('Unpublishing local media (' + stream.connection.connectionId + ')'); this.openvidu.sendRequest('unpublishVideo', (error, response) => { if (error) { console.error(error); } else { console.info('Media unpublished correctly'); } }); stream.disposeWebRtcPeer(); delete stream.connection.stream; const streamEvent = new StreamEvent(true, publisher, 'streamDestroyed', publisher.stream, 'unpublish'); publisher.emitEvent('streamDestroyed', [streamEvent]); streamEvent.callDefaultBehavior(); } } /** * Forces some user to leave the session * * #### Events dispatched * * The behavior is the same as when some user calls [[Session.disconnect]], but `reason` property in all events will be `"forceDisconnectByUser"`. * * The [[Session]] object of every participant will dispatch a `streamDestroyed` event if the evicted user was publishing a stream, with property `reason` set to `"forceDisconnectByUser"`. * The [[Session]] object of every participant except the evicted one will dispatch a `connectionDestroyed` event for the evicted user, with property `reason` set to `"forceDisconnectByUser"`. * * If any, the [[Publisher]] object of the evicted participant will also dispatch a `streamDestroyed` event with property `reason` set to `"forceDisconnectByUser"`. * The [[Session]] object of the evicted participant will dispatch a `sessionDisconnected` event with property `reason` set to `"forceDisconnectByUser"`. * * See [[StreamEvent]], [[ConnectionEvent]] and [[SessionDisconnectedEvent]] to learn more. * * @returns A Promise (to which you can optionally subscribe to) that is resolved only after the participant has been successfully evicted from the session and rejected with an Error object if not */ forceDisconnect(connection: Connection): Promise { return new Promise((resolve, reject) => { console.info('Forcing disconnect for connection ' + connection.connectionId); this.openvidu.sendRequest( 'forceDisconnect', { connectionId: connection.connectionId }, (error, response) => { if (error) { console.error('Error forcing disconnect for Connection ' + connection.connectionId, error); if (error.code === 401) { reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to force a disconnection")); } else { reject(error); } } else { console.info('Forcing disconnect correctly for Connection ' + connection.connectionId); resolve(); } } ); }); } /** * Forces some user to unpublish a Stream * * #### Events dispatched * * The behavior is the same as when some user calls [[Session.unpublish]], but `reason` property in all events will be `"forceUnpublishByUser"` * * The [[Session]] object of every participant will dispatch a `streamDestroyed` event with property `reason` set to `"forceDisconnectByUser"` * * The [[Publisher]] object of the affected participant will also dispatch a `streamDestroyed` event with property `reason` set to `"forceDisconnectByUser"` * * See [[StreamEvent]] to learn more. * * @returns A Promise (to which you can optionally subscribe to) that is resolved only after the remote Stream has been successfully unpublished from the session and rejected with an Error object if not */ forceUnpublish(stream: Stream): Promise { return new Promise((resolve, reject) => { console.info('Forcing unpublish for stream ' + stream.streamId); this.openvidu.sendRequest( 'forceUnpublish', { streamId: stream.streamId }, (error, response) => { if (error) { console.error('Error forcing unpublish for Stream ' + stream.streamId, error); if (error.code === 401) { reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to force an unpublishing")); } else { reject(error); } } else { console.info('Forcing unpublish correctly for Stream ' + stream.streamId); resolve(); } } ); }); } /** * Sends one signal. `signal` object has the following optional properties: * ```json * {data:string, to:Connection[], type:string} * ``` * All users subscribed to that signal (`session.on('signal:type', ...)` or `session.on('signal', ...)` for all signals) and whose Connection objects are in `to` array will receive it. Their local * Session objects will dispatch a `signal` or `signal:type` event. See [[SignalEvent]] to learn more. * * @returns A Promise (to which you can optionally subscribe to) that is resolved if the message successfully reached openvidu-server and rejected with an Error object if not. _This doesn't * mean that openvidu-server could resend the message to all the listed receivers._ */ /* tslint:disable:no-string-literal */ signal(signal: SignalOptions): Promise { return new Promise((resolve, reject) => { const signalMessage = {}; if (signal.to && signal.to.length > 0) { const connectionIds: string[] = []; signal.to.forEach(connection => { connectionIds.push(connection.connectionId); }); signalMessage['to'] = connectionIds; } else { signalMessage['to'] = []; } signalMessage['data'] = signal.data ? signal.data : ''; let typeAux: string = signal.type ? signal.type : 'signal'; if (!!typeAux) { if (typeAux.substring(0, 7) !== 'signal:') { typeAux = 'signal:' + typeAux; } } signalMessage['type'] = typeAux; this.openvidu.sendRequest('sendMessage', { message: JSON.stringify(signalMessage) }, (error, response) => { if (!!error) { reject(error); } else { resolve(); } }); }); } /* tslint:enable:no-string-literal */ /** * See [[EventDispatcher.on]] */ on(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent) => void): EventDispatcher { this.ee.on(type, event => { if (event) { console.info("Event '" + type + "' triggered by 'Session'", event); } else { console.info("Event '" + type + "' triggered by 'Session'"); } handler(event); }); if (type === 'publisherStartSpeaking' || type === 'publisherStopSpeaking') { this.speakingEventsEnabled = true; // If there are already available remote streams, enable hark 'speaking' event in all of them for (const connectionId in this.remoteConnections) { const str = this.remoteConnections[connectionId].stream; if (!!str && str.hasAudio) { str.enableSpeakingEvents(); } } } return this; } /** * See [[EventDispatcher.once]] */ once(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent) => void): Session { this.ee.once(type, event => { if (event) { console.info("Event '" + type + "' triggered by 'Session'", event); } else { console.info("Event '" + type + "' triggered by 'Session'"); } handler(event); }); if (type === 'publisherStartSpeaking' || type === 'publisherStopSpeaking') { this.speakingEventsEnabled = true; // If there are already available remote streams, enable hark in all of them for (const connectionId in this.remoteConnections) { const str = this.remoteConnections[connectionId].stream; if (!!str && str.hasAudio) { str.enableOnceSpeakingEvents(); } } } return this; } /** * See [[EventDispatcher.off]] */ off(type: string, handler?: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent) => void): Session { if (!handler) { this.ee.removeAllListeners(type); } else { this.ee.off(type, handler); } if (type === 'publisherStartSpeaking' || type === 'publisherStopSpeaking') { this.speakingEventsEnabled = false; // If there are already available remote streams, disable hark in all of them for (const connectionId in this.remoteConnections) { const str = this.remoteConnections[connectionId].stream; if (!!str) { str.disableSpeakingEvents(); } } } return this; } /* Hidden methods */ /** * @hidden */ onParticipantJoined(response: ConnectionOptions): void { // Connection shouldn't exist this.getConnection(response.id, '') .then(connection => { console.warn('Connection ' + response.id + ' already exists in connections list'); }) .catch(openViduError => { const connection = new Connection(this, response); this.remoteConnections[response.id] = connection; this.ee.emitEvent('connectionCreated', [new ConnectionEvent(false, this, 'connectionCreated', connection, '')]); }); } /** * @hidden */ onParticipantLeft(msg): void { this.getRemoteConnection(msg.connectionId, 'Remote connection ' + msg.connectionId + " unknown when 'onParticipantLeft'. " + 'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections))) .then(connection => { if (!!connection.stream) { const stream = connection.stream; const streamEvent = new StreamEvent(true, this, 'streamDestroyed', stream, msg.reason); this.ee.emitEvent('streamDestroyed', [streamEvent]); streamEvent.callDefaultBehavior(); delete this.remoteStreamsCreated[stream.streamId]; - if (Object.keys(this.remoteStreamsCreated).length === 0) { - this.isFirstIonicIosSubscriber = true; - this.countDownForIonicIosSubscribers = true; - } } delete this.remoteConnections[connection.connectionId]; this.ee.emitEvent('connectionDestroyed', [new ConnectionEvent(false, this, 'connectionDestroyed', connection, msg.reason)]); }) .catch(openViduError => { console.error(openViduError); }); } /** * @hidden */ onParticipantPublished(response: ConnectionOptions): void { const afterConnectionFound = (connection) => { this.remoteConnections[connection.connectionId] = connection; if (!this.remoteStreamsCreated[connection.stream.streamId]) { // Avoid race condition between stream.subscribe() in "onParticipantPublished" and in "joinRoom" rpc callback // This condition is false if openvidu-server sends "participantPublished" event to a subscriber participant that has // already subscribed to certain stream in the callback of "joinRoom" method this.ee.emitEvent('streamCreated', [new StreamEvent(false, this, 'streamCreated', connection.stream, '')]); } this.remoteStreamsCreated[connection.stream.streamId] = true; }; // Get the existing Connection created on 'onParticipantJoined' for // existing participants or create a new one for new participants let connection: Connection; this.getRemoteConnection(response.id, "Remote connection '" + response.id + "' unknown when 'onParticipantPublished'. " + 'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections))) .then(con => { // Update existing Connection connection = con; response.metadata = con.data; connection.options = response; connection.initRemoteStreams(response.streams); afterConnectionFound(connection); }) .catch(openViduError => { // Create new Connection connection = new Connection(this, response); afterConnectionFound(connection); }); } /** * @hidden */ onParticipantUnpublished(msg): void { if (msg.connectionId === this.connection.connectionId) { // Your stream has been forcedly unpublished from the session this.stopPublisherStream(msg.reason); } else { this.getRemoteConnection(msg.connectionId, "Remote connection '" + msg.connectionId + "' unknown when 'onParticipantUnpublished'. " + 'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections))) .then(connection => { const streamEvent = new StreamEvent(true, this, 'streamDestroyed', connection.stream, msg.reason); this.ee.emitEvent('streamDestroyed', [streamEvent]); streamEvent.callDefaultBehavior(); // Deleting the remote stream const streamId: string = connection.stream.streamId; delete this.remoteStreamsCreated[streamId]; - if (Object.keys(this.remoteStreamsCreated).length === 0) { - this.isFirstIonicIosSubscriber = true; - this.countDownForIonicIosSubscribers = true; - } connection.removeStream(streamId); }) .catch(openViduError => { console.error(openViduError); }); } } /** * @hidden */ onParticipantEvicted(msg): void { if (msg.connectionId === this.connection.connectionId) { // You have been evicted from the session if (!!this.sessionId && !this.connection.disposed) { this.leave(true, msg.reason); } } } /** * @hidden */ onNewMessage(msg): void { console.info('New signal: ' + JSON.stringify(msg)); this.getConnection(msg.from, "Connection '" + msg.from + "' unknow when 'onNewMessage'. Existing remote connections: " + JSON.stringify(Object.keys(this.remoteConnections)) + '. Existing local connection: ' + this.connection.connectionId) .then(connection => { this.ee.emitEvent('signal', [new SignalEvent(this, msg.type, msg.data, connection)]); if (msg.type !== 'signal') { this.ee.emitEvent(msg.type, [new SignalEvent(this, msg.type, msg.data, connection)]); } }) .catch(openViduError => { console.error(openViduError); }); } /** * @hidden */ onStreamPropertyChanged(msg): void { const callback = (connection: Connection) => { if (!!connection.stream && connection.stream.streamId === msg.streamId) { const stream = connection.stream; let oldValue; switch (msg.property) { case 'audioActive': oldValue = stream.audioActive; msg.newValue = msg.newValue === 'true'; stream.audioActive = msg.newValue; break; case 'videoActive': oldValue = stream.videoActive; msg.newValue = msg.newValue === 'true'; stream.videoActive = msg.newValue; break; case 'videoDimensions': oldValue = stream.videoDimensions; msg.newValue = JSON.parse(JSON.parse(msg.newValue)); stream.videoDimensions = msg.newValue; break; case 'filter': oldValue = stream.filter; msg.newValue = (Object.keys(msg.newValue).length > 0) ? msg.newValue : undefined; if (msg.newValue !== undefined) { stream.filter = new Filter(msg.newValue.type, msg.newValue.options); stream.filter.stream = stream; if (msg.newValue.lastExecMethod) { stream.filter.lastExecMethod = msg.newValue.lastExecMethod; } } else { delete stream.filter; } msg.newValue = stream.filter; break; } this.ee.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this, stream, msg.property, msg.newValue, oldValue, msg.reason)]); if (!!stream.streamManager) { stream.streamManager.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(stream.streamManager, stream, msg.property, msg.newValue, oldValue, msg.reason)]); } } else { console.error("No stream with streamId '" + msg.streamId + "' found for connection '" + msg.connectionId + "' on 'streamPropertyChanged' event"); } }; if (msg.connectionId === this.connection.connectionId) { // Your stream has been forcedly changed (filter feature) callback(this.connection); } else { this.getRemoteConnection(msg.connectionId, 'Remote connection ' + msg.connectionId + " unknown when 'onStreamPropertyChanged'. " + 'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections))) .then(connection => { callback(connection); }) .catch(openViduError => { console.error(openViduError); }); } } /** * @hidden */ recvIceCandidate(msg): void { const candidate: RTCIceCandidate = { candidate: msg.candidate, component: msg.component, foundation: msg.foundation, ip: msg.ip, port: msg.port, priority: msg.priority, protocol: msg.protocol, relatedAddress: msg.relatedAddress, relatedPort: msg.relatedPort, sdpMid: msg.sdpMid, sdpMLineIndex: msg.sdpMLineIndex, tcpType: msg.tcpType, usernameFragment: msg.usernameFragment, type: msg.type, toJSON: () => { return { candidate: msg.candidate }; } }; this.getConnection(msg.senderConnectionId, 'Connection not found for connectionId ' + msg.senderConnectionId + ' owning endpoint ' + msg.endpointName + '. Ice candidate will be ignored: ' + candidate) .then(connection => { const stream = connection.stream; stream.getWebRtcPeer().addIceCandidate(candidate).catch(error => { console.error('Error adding candidate for ' + stream.streamId + ' stream of endpoint ' + msg.endpointName + ': ' + error); }); }) .catch(openViduError => { console.error(openViduError); }); } /** * @hidden */ onSessionClosed(msg): void { console.info('Session closed: ' + JSON.stringify(msg)); const s = msg.sessionId; if (s !== undefined) { this.ee.emitEvent('session-closed', [{ session: s }]); } else { console.warn('Session undefined on session closed', msg); } } /** * @hidden */ onLostConnection(reason: string): void { console.warn('Lost connection in session ' + this.sessionId + ' waiting for reconnect'); if (!!this.sessionId && !this.connection.disposed) { this.leave(true, reason); } } /** * @hidden */ onRecoveredConnection(): void { console.warn('Recovered connection in Session ' + this.sessionId); // this.ee.emitEvent('connectionRecovered', []); } /** * @hidden */ onMediaError(params): void { console.error('Media error: ' + JSON.stringify(params)); const err = params.error; if (err) { this.ee.emitEvent('error-media', [{ error: err }]); } else { console.warn('Received undefined media error. Params:', params); } } /** * @hidden */ onRecordingStarted(response): void { this.ee.emitEvent('recordingStarted', [new RecordingEvent(this, 'recordingStarted', response.id, response.name)]); } /** * @hidden */ onRecordingStopped(response): void { this.ee.emitEvent('recordingStopped', [new RecordingEvent(this, 'recordingStopped', response.id, response.name, response.reason)]); } /** * @hidden * response = {connectionId: string, streamId: string, type: string, data: Object} */ onFilterEventDispatched(response): void { const connectionId: string = response.connectionId; const streamId: string = response.streamId; this.getConnection(connectionId, 'No connection found for connectionId ' + connectionId) .then(connection => { console.info('Filter event dispatched'); const stream: Stream = connection.stream; stream.filter.handlers[response.eventType](new FilterEvent(stream.filter, response.eventType, response.data)); }); } /** * @hidden */ emitEvent(type: string, eventArray: any[]): void { this.ee.emitEvent(type, eventArray); } /** * @hidden */ leave(forced: boolean, reason: string): void { forced = !!forced; console.info('Leaving Session (forced=' + forced + ')'); if (!!this.connection) { if (!this.connection.disposed && !forced) { this.openvidu.sendRequest('leaveRoom', (error, response) => { if (error) { console.error(error); } this.openvidu.closeWs(); }); } else { this.openvidu.closeWs(); } this.stopPublisherStream(reason); if (!this.connection.disposed) { // Make Session object dispatch 'sessionDisconnected' event (if it is not already disposed) const sessionDisconnectEvent = new SessionDisconnectedEvent(this, reason); this.ee.emitEvent('sessionDisconnected', [sessionDisconnectEvent]); sessionDisconnectEvent.callDefaultBehavior(); } } else { console.warn('You were not connected to the session ' + this.sessionId); } } /* Private methods */ private connectAux(token: string): Promise { return new Promise((resolve, reject) => { this.openvidu.startWs((error) => { if (!!error) { reject(error); } else { const joinParams = { token: (!!token) ? token : '', session: this.sessionId, platform: !!platform.description ? platform.description : 'unknown', metadata: !!this.options.metadata ? this.options.metadata : '', secret: this.openvidu.getSecret(), recorder: this.openvidu.getRecorder() }; this.openvidu.sendRequest('joinRoom', joinParams, (error, response) => { if (!!error) { reject(error); } else { // Initialize capabilities object with the role this.capabilities = { subscribe: true, publish: this.openvidu.role !== 'SUBSCRIBER', forceUnpublish: this.openvidu.role === 'MODERATOR', forceDisconnect: this.openvidu.role === 'MODERATOR' }; // Initialize local Connection object with values returned by openvidu-server this.connection = new Connection(this); this.connection.connectionId = response.id; this.connection.creationTime = response.createdAt; this.connection.data = response.metadata; this.connection.rpcSessionId = response.sessionId; // Initialize remote Connections with value returned by openvidu-server const events = { connections: new Array(), streams: new Array() }; const existingParticipants: ConnectionOptions[] = response.value; existingParticipants.forEach(participant => { const connection = new Connection(this, participant); this.remoteConnections[connection.connectionId] = connection; events.connections.push(connection); if (!!connection.stream) { this.remoteStreamsCreated[connection.stream.streamId] = true; events.streams.push(connection.stream); } }); // Own 'connectionCreated' event this.ee.emitEvent('connectionCreated', [new ConnectionEvent(false, this, 'connectionCreated', this.connection, '')]); // One 'connectionCreated' event for each existing connection in the session events.connections.forEach(connection => { this.ee.emitEvent('connectionCreated', [new ConnectionEvent(false, this, 'connectionCreated', connection, '')]); }); // One 'streamCreated' event for each active stream in the session events.streams.forEach(stream => { this.ee.emitEvent('streamCreated', [new StreamEvent(false, this, 'streamCreated', stream, '')]); }); resolve(); } }); } }); }); } private stopPublisherStream(reason: string) { if (!!this.connection.stream) { // Dispose Publisher's local stream this.connection.stream.disposeWebRtcPeer(); if (this.connection.stream.isLocalStreamPublished) { // Make Publisher object dispatch 'streamDestroyed' event if the Stream was published this.connection.stream.ee.emitEvent('local-stream-destroyed', [reason]); } } } private stringClientMetadata(metadata: any): string { if (typeof metadata !== 'string') { return JSON.stringify(metadata); } else { return metadata; } } private getConnection(connectionId: string, errorMessage: string): Promise { return new Promise((resolve, reject) => { const connection = this.remoteConnections[connectionId]; if (!!connection) { // Resolve remote connection resolve(connection); } else { if (this.connection.connectionId === connectionId) { // Resolve local connection resolve(this.connection); } else { // Connection not found. Reject with OpenViduError reject(new OpenViduError(OpenViduErrorName.GENERIC_ERROR, errorMessage)); } } }); } private getRemoteConnection(connectionId: string, errorMessage: string): Promise { return new Promise((resolve, reject) => { const connection = this.remoteConnections[connectionId]; if (!!connection) { // Resolve remote connection resolve(connection); } else { // Remote connection not found. Reject with OpenViduError reject(new OpenViduError(OpenViduErrorName.GENERIC_ERROR, errorMessage)); } }); } private processToken(token: string): void { const match = token.match(/^(wss?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/); if (!!match) { const url = { protocol: match[1], host: match[2], hostname: match[3], port: match[4], pathname: match[5], search: match[6], hash: match[7] }; const params = token.split('?'); const queryParams = decodeURI(params[1]) .split('&') .map(param => param.split('=')) .reduce((values, [key, value]) => { values[key] = value return values }, {}); this.sessionId = queryParams['sessionId']; const secret = queryParams['secret']; const recorder = queryParams['recorder']; const turnUsername = queryParams['turnUsername']; const turnCredential = queryParams['turnCredential']; const role = queryParams['role']; const webrtcStatsInterval = queryParams['webrtcStatsInterval']; const openviduServerVersion = queryParams['version']; if (!!secret) { this.openvidu.secret = secret; } if (!!recorder) { this.openvidu.recorder = true; } if (!!turnUsername && !!turnCredential) { const stunUrl = 'stun:' + url.hostname + ':3478'; const turnUrl1 = 'turn:' + url.hostname + ':3478'; const turnUrl2 = turnUrl1 + '?transport=tcp'; this.openvidu.iceServers = [ { urls: [stunUrl] }, { urls: [turnUrl1, turnUrl2], username: turnUsername, credential: turnCredential } ]; console.log('TURN temp credentials [' + turnUsername + ':' + turnCredential + ']'); } if (!!role) { this.openvidu.role = role; } if (!!webrtcStatsInterval) { this.openvidu.webrtcStatsInterval = +webrtcStatsInterval; } if (!!openviduServerVersion) { console.info("openvidu-server version: " + openviduServerVersion); if (openviduServerVersion !== this.openvidu.libraryVersion) { console.error('OpenVidu Server (' + openviduServerVersion + ') and OpenVidu Browser (' + this.openvidu.libraryVersion + ') versions do NOT match. There may be incompatibilities') } } this.openvidu.wsUri = 'wss://' + url.host + '/openvidu'; this.openvidu.httpUri = 'https://' + url.host; } else { console.error('Token "' + token + '" is not valid') } } } diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index b58a7411..de2b214f 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -1,1025 +1,1016 @@ /* * (C) Copyright 2017-2019 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 { Connection } from './Connection'; import { Event } from '../OpenViduInternal/Events/Event'; import { Filter } from './Filter'; import { Session } from './Session'; import { StreamManager } from './StreamManager'; import { Subscriber } from './Subscriber'; import { EventDispatcher } from '../OpenViduInternal/Interfaces/Public/EventDispatcher'; import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions'; import { OutboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/OutboundStreamOptions'; import { WebRtcPeer, WebRtcPeerSendonly, WebRtcPeerRecvonly, WebRtcPeerSendrecv } from '../OpenViduInternal/WebRtcPeer/WebRtcPeer'; import { WebRtcStats } from '../OpenViduInternal/WebRtcStats/WebRtcStats'; import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpeakingEvent'; import { StreamManagerEvent } from '../OpenViduInternal/Events/StreamManagerEvent'; import { StreamPropertyChangedEvent } from '../OpenViduInternal/Events/StreamPropertyChangedEvent'; import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; import EventEmitter = require('wolfy87-eventemitter'); import hark = require('hark'); import platform = require('platform'); /** * 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 Stream implements EventDispatcher { /** * The Connection object that is publishing the stream */ connection: Connection; /** * Frame rate of the video in frames per second. This property is only defined if the [[Publisher]] of * the stream was initialized passing a _frameRate_ property on [[OpenVidu.initPublisher]] method */ frameRate?: number; /** * Whether the stream has a video track or not */ hasVideo: boolean; /** * Whether the stream has an audio track or not */ hasAudio: boolean; /** * Whether the stream has the video track muted or unmuted. If [[hasVideo]] is false, this property is undefined. * * This property may change if the Publisher publishing the stream calls [[Publisher.publishVideo]]. Whenever this happens a [[StreamPropertyChangedEvent]] will be dispatched * by the Session object as well as by the affected Subscriber/Publisher object */ videoActive: boolean; /** * Whether the stream has the audio track muted or unmuted. If [[hasAudio]] is false, this property is undefined * * This property may change if the Publisher publishing the stream calls [[Publisher.publishAudio]]. Whenever this happens a [[StreamPropertyChangedEvent]] will be dispatched * by the Session object as well as by the affected Subscriber/Publisher object */ audioActive: boolean; /** * Unique identifier of the stream. If the stream belongs to a... * - Subscriber object: property `streamId` is always defined * - Publisher object: property `streamId` is only defined after successful execution of [[Session.publish]] */ streamId: string; /** * Time when this stream was created in OpenVidu Server (UTC milliseconds). Depending on the owner of this stream: * - Subscriber object: property `creationTime` is always defined * - Publisher object: property `creationTime` is only defined after successful execution of [[Session.publish]] */ creationTime: number; /** * `"CAMERA"`, `"SCREEN"` or `"CUSTOM"` (the latter when [[PublisherProperties.videoSource]] is a MediaStreamTrack when calling [[OpenVidu.initPublisher]]). * If [[hasVideo]] is false, this property is undefined */ typeOfVideo?: string; /** * StreamManager object ([[Publisher]] or [[Subscriber]]) in charge of displaying this stream in the DOM */ streamManager: StreamManager; /** * Width and height in pixels of the encoded video stream. If [[hasVideo]] is false, this property is undefined * * This property may change if the Publisher that is publishing: * - If it is a mobile device, whenever the user rotates the device. * - If it is screen-sharing, whenever the user changes the size of the captured window. * * Whenever this happens a [[StreamPropertyChangedEvent]] will be dispatched by the Session object as well as by the affected Subscriber/Publisher object */ videoDimensions: { width: number, height: number }; /** * **WARNING**: experimental option. This interface may change in the near future * * Filter applied to the Stream. You can apply filters by calling [[Stream.applyFilter]], execute methods of the applied filter with * [[Filter.execMethod]] and remove it with [[Stream.removeFilter]]. Be aware that the client calling this methods must have the * necessary permissions: the token owned by the client must have been initialized with the appropriated `allowedFilters` array. */ filter: Filter; /** * @hidden */ ee = new EventEmitter(); private webRtcPeer: WebRtcPeer; private mediaStream: MediaStream; private webRtcStats: WebRtcStats; private isSubscribeToRemote = false; /** * @hidden */ isLocalStreamReadyToPublish = false; /** * @hidden */ isLocalStreamPublished = false; /** * @hidden */ publishedOnce = false; /** * @hidden */ session: Session; /** * @hidden */ inboundStreamOpts: InboundStreamOptions; /** * @hidden */ outboundStreamOpts: OutboundStreamOptions; /** * @hidden */ speechEvent: any; /** * @hidden */ publisherStartSpeakingEventEnabled = false; /** * @hidden */ publisherStopSpeakingEventEnabled = false; /** * @hidden */ volumeChangeEventEnabled = false; /** * @hidden */ constructor(session: Session, options: InboundStreamOptions | OutboundStreamOptions | {}) { this.session = session; if (options.hasOwnProperty('id')) { // InboundStreamOptions: stream belongs to a Subscriber this.inboundStreamOpts = options; this.streamId = this.inboundStreamOpts.id; this.creationTime = this.inboundStreamOpts.createdAt; this.hasAudio = this.inboundStreamOpts.hasAudio; this.hasVideo = this.inboundStreamOpts.hasVideo; if (this.hasAudio) { this.audioActive = this.inboundStreamOpts.audioActive; } if (this.hasVideo) { this.videoActive = this.inboundStreamOpts.videoActive; this.typeOfVideo = (!this.inboundStreamOpts.typeOfVideo) ? undefined : this.inboundStreamOpts.typeOfVideo; this.frameRate = (this.inboundStreamOpts.frameRate === -1) ? undefined : this.inboundStreamOpts.frameRate; this.videoDimensions = this.inboundStreamOpts.videoDimensions; } if (!!this.inboundStreamOpts.filter && (Object.keys(this.inboundStreamOpts.filter).length > 0)) { if (!!this.inboundStreamOpts.filter.lastExecMethod && Object.keys(this.inboundStreamOpts.filter.lastExecMethod).length === 0) { delete this.inboundStreamOpts.filter.lastExecMethod; } this.filter = this.inboundStreamOpts.filter; } } else { // OutboundStreamOptions: stream belongs to a Publisher this.outboundStreamOpts = options; this.hasAudio = this.isSendAudio(); this.hasVideo = this.isSendVideo(); if (this.hasAudio) { this.audioActive = !!this.outboundStreamOpts.publisherProperties.publishAudio; } if (this.hasVideo) { this.videoActive = !!this.outboundStreamOpts.publisherProperties.publishVideo; this.frameRate = this.outboundStreamOpts.publisherProperties.frameRate; if (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) { this.typeOfVideo = 'CUSTOM'; } else { this.typeOfVideo = this.isSendScreen() ? 'SCREEN' : 'CAMERA'; } } if (!!this.outboundStreamOpts.publisherProperties.filter) { this.filter = this.outboundStreamOpts.publisherProperties.filter; } } this.ee.on('mediastream-updated', () => { this.streamManager.updateMediaStream(this.mediaStream); console.debug('Video srcObject [' + this.mediaStream + '] updated in stream [' + this.streamId + ']'); }); } /** * See [[EventDispatcher.on]] */ on(type: string, handler: (event: Event) => void): EventDispatcher { this.ee.on(type, event => { if (event) { console.info("Event '" + type + "' triggered by stream '" + this.streamId + "'", event); } else { console.info("Event '" + type + "' triggered by stream '" + this.streamId + "'"); } handler(event); }); return this; } /** * See [[EventDispatcher.once]] */ once(type: string, handler: (event: Event) => void): EventDispatcher { this.ee.once(type, event => { if (event) { console.info("Event '" + type + "' triggered once by stream '" + this.streamId + "'", event); } else { console.info("Event '" + type + "' triggered once by stream '" + this.streamId + "'"); } handler(event); }); return this; } /** * See [[EventDispatcher.off]] */ off(type: string, handler?: (event: Event) => void): EventDispatcher { if (!handler) { this.ee.removeAllListeners(type); } else { this.ee.off(type, handler); } return this; } /** * Applies an audio/video filter to the stream. * * @param type Type of filter applied. See [[Filter.type]] * @param options Parameters used to initialize the filter. See [[Filter.options]] * * @returns A Promise (to which you can optionally subscribe to) that is resolved to the applied filter if success and rejected with an Error object if not */ applyFilter(type: string, options: Object): Promise { return new Promise((resolve, reject) => { console.info('Applying filter to stream ' + this.streamId); options = !!options ? options : {}; if (typeof options !== 'string') { options = JSON.stringify(options); } this.session.openvidu.sendRequest( 'applyFilter', { streamId: this.streamId, type, options }, (error, response) => { if (error) { console.error('Error applying filter for Stream ' + this.streamId, error); if (error.code === 401) { reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to apply a filter")); } else { reject(error); } } else { console.info('Filter successfully applied on Stream ' + this.streamId); const oldValue: Filter = this.filter; this.filter = new Filter(type, options); this.filter.stream = this; this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this, 'filter', this.filter, oldValue, 'applyFilter')]); this.streamManager.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.streamManager, this, 'filter', this.filter, oldValue, 'applyFilter')]); resolve(this.filter); } } ); }); } /** * Removes an audio/video filter previously applied. * * @returns A Promise (to which you can optionally subscribe to) that is resolved if the previously applied filter has been successfully removed and rejected with an Error object in other case */ removeFilter(): Promise { return new Promise((resolve, reject) => { console.info('Removing filter of stream ' + this.streamId); this.session.openvidu.sendRequest( 'removeFilter', { streamId: this.streamId }, (error, response) => { if (error) { console.error('Error removing filter for Stream ' + this.streamId, error); if (error.code === 401) { reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to remove a filter")); } else { reject(error); } } else { console.info('Filter successfully removed from Stream ' + this.streamId); const oldValue = this.filter; delete this.filter; this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this, 'filter', this.filter, oldValue, 'applyFilter')]); this.streamManager.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.streamManager, this, 'filter', this.filter, oldValue, 'applyFilter')]); resolve(); } } ); }); } /* Hidden methods */ /** * @hidden */ getMediaStream(): MediaStream { return this.mediaStream; } /** * @hidden */ setMediaStream(mediaStream: MediaStream): void { this.mediaStream = mediaStream; } /** * @hidden */ updateMediaStreamInVideos() { this.ee.emitEvent('mediastream-updated', []); } /** * @hidden */ getWebRtcPeer(): WebRtcPeer { return this.webRtcPeer; } /** * @hidden */ getRTCPeerConnection(): RTCPeerConnection { return this.webRtcPeer.pc; } /** * @hidden */ subscribeToMyRemote(value: boolean): void { this.isSubscribeToRemote = value; } /** * @hidden */ setOutboundStreamOptions(outboundStreamOpts: OutboundStreamOptions): void { this.outboundStreamOpts = outboundStreamOpts; } /** * @hidden */ subscribe(): Promise { return new Promise((resolve, reject) => { this.initWebRtcPeerReceive() .then(() => { resolve(); }) .catch(error => { reject(error); }); }); } /** * @hidden */ publish(): Promise { return new Promise((resolve, reject) => { if (this.isLocalStreamReadyToPublish) { this.initWebRtcPeerSend() .then(() => { resolve(); }) .catch(error => { reject(error); }); } else { this.ee.once('stream-ready-to-publish', () => { this.publish() .then(() => { resolve(); }) .catch(error => { reject(error); }); }); } }); } /** * @hidden */ disposeWebRtcPeer(): void { if (this.webRtcPeer) { const isSenderAndCustomTrack: boolean = !!this.outboundStreamOpts && typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack; this.webRtcPeer.dispose(isSenderAndCustomTrack); } if (this.speechEvent) { this.speechEvent.stop(); delete this.speechEvent; } this.stopWebRtcStats(); console.info((!!this.outboundStreamOpts ? 'Outbound ' : 'Inbound ') + "WebRTCPeer from 'Stream' with id [" + this.streamId + '] is now closed'); } /** * @hidden */ disposeMediaStream(): void { if (this.mediaStream) { this.mediaStream.getAudioTracks().forEach((track) => { track.stop(); }); this.mediaStream.getVideoTracks().forEach((track) => { track.stop(); }); delete this.mediaStream; } console.info((!!this.outboundStreamOpts ? 'Local ' : 'Remote ') + "MediaStream from 'Stream' with id [" + this.streamId + '] is now disposed'); } /** * @hidden */ displayMyRemote(): boolean { return this.isSubscribeToRemote; } /** * @hidden */ isSendAudio(): boolean { return (!!this.outboundStreamOpts && this.outboundStreamOpts.publisherProperties.audioSource !== null && this.outboundStreamOpts.publisherProperties.audioSource !== false); } /** * @hidden */ isSendVideo(): boolean { return (!!this.outboundStreamOpts && this.outboundStreamOpts.publisherProperties.videoSource !== null && this.outboundStreamOpts.publisherProperties.videoSource !== false); } /** * @hidden */ isSendScreen(): boolean { let screen = this.outboundStreamOpts.publisherProperties.videoSource === 'screen'; if (platform.name === 'Electron') { screen = typeof this.outboundStreamOpts.publisherProperties.videoSource === 'string' && this.outboundStreamOpts.publisherProperties.videoSource.startsWith('screen:'); } return !!this.outboundStreamOpts && screen; } /** * @hidden */ setSpeechEventIfNotExists(): void { if (!this.speechEvent) { const harkOptions = this.session.openvidu.advancedConfiguration.publisherSpeakingEventsOptions || {}; harkOptions.interval = (typeof harkOptions.interval === 'number') ? harkOptions.interval : 50; harkOptions.threshold = (typeof harkOptions.threshold === 'number') ? harkOptions.threshold : -50; this.speechEvent = hark(this.mediaStream, harkOptions); } } /** * @hidden */ enableSpeakingEvents(): void { this.setSpeechEventIfNotExists(); if (!this.publisherStartSpeakingEventEnabled) { this.publisherStartSpeakingEventEnabled = true; this.speechEvent.on('speaking', () => { this.session.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStartSpeaking', this.connection, this.streamId)]); }); } if (!this.publisherStopSpeakingEventEnabled) { this.publisherStopSpeakingEventEnabled = true; this.speechEvent.on('stopped_speaking', () => { this.session.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId)]); }); } } /** * @hidden */ enableOnceSpeakingEvents(): void { this.setSpeechEventIfNotExists(); if (!this.publisherStartSpeakingEventEnabled) { this.publisherStartSpeakingEventEnabled = true; this.speechEvent.once('speaking', () => { this.session.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStartSpeaking', this.connection, this.streamId)]); this.disableSpeakingEvents(); }); } if (!this.publisherStopSpeakingEventEnabled) { this.publisherStopSpeakingEventEnabled = true; this.speechEvent.once('stopped_speaking', () => { this.session.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId)]); this.disableSpeakingEvents(); }); } } /** * @hidden */ disableSpeakingEvents(): void { if (!!this.speechEvent) { if (this.volumeChangeEventEnabled) { // 'streamAudioVolumeChange' event is enabled. Cannot stop the hark process this.speechEvent.off('speaking'); this.speechEvent.off('stopped_speaking'); } else { this.speechEvent.stop(); delete this.speechEvent; } } this.publisherStartSpeakingEventEnabled = false; this.publisherStopSpeakingEventEnabled = false; } /** * @hidden */ enableVolumeChangeEvent(): void { this.setSpeechEventIfNotExists(); if (!this.volumeChangeEventEnabled) { this.volumeChangeEventEnabled = true; this.speechEvent.on('volume_change', harkEvent => { const oldValue = this.speechEvent.oldVolumeValue; const value = { newValue: harkEvent, oldValue }; this.speechEvent.oldVolumeValue = harkEvent; this.streamManager.emitEvent('streamAudioVolumeChange', [new StreamManagerEvent(this.streamManager, 'streamAudioVolumeChange', value)]); }); } } /** * @hidden */ enableOnceVolumeChangeEvent(): void { this.setSpeechEventIfNotExists(); if (!this.volumeChangeEventEnabled) { this.volumeChangeEventEnabled = true; this.speechEvent.once('volume_change', harkEvent => { const oldValue = this.speechEvent.oldVolumeValue; const value = { newValue: harkEvent, oldValue }; this.speechEvent.oldVolumeValue = harkEvent; this.disableVolumeChangeEvent(); this.streamManager.emitEvent('streamAudioVolumeChange', [new StreamManagerEvent(this.streamManager, 'streamAudioVolumeChange', value)]); }); } } /** * @hidden */ disableVolumeChangeEvent(): void { if (!!this.speechEvent) { if (this.session.speakingEventsEnabled) { // 'publisherStartSpeaking' and/or publisherStopSpeaking` events are enabled. Cannot stop the hark process this.speechEvent.off('volume_change'); } else { this.speechEvent.stop(); delete this.speechEvent; } } this.volumeChangeEventEnabled = false; } /** * @hidden */ isLocal(): boolean { // inbound options undefined and outbound options defined return (!this.inboundStreamOpts && !!this.outboundStreamOpts); } /** * @hidden */ getSelectedIceCandidate(): Promise { return new Promise((resolve, reject) => { this.webRtcStats.getSelectedIceCandidateInfo() .then(report => resolve(report)) .catch(error => reject(error)); }); } /** * @hidden */ getRemoteIceCandidateList(): RTCIceCandidate[] { return this.webRtcPeer.remoteCandidatesQueue; } /** * @hidden */ getLocalIceCandidateList(): RTCIceCandidate[] { return this.webRtcPeer.localCandidatesQueue; } /* Private methods */ private initWebRtcPeerSend(): Promise { return new Promise((resolve, reject) => { 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) => { console.debug('Sending SDP offer to publish as ' + this.streamId, sdpOfferParam); let typeOfVideo = ''; if (this.isSendVideo()) { typeOfVideo = (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) ? 'CUSTOM' : (this.isSendScreen() ? 'SCREEN' : 'CAMERA'); } this.session.openvidu.sendRequest('publishVideo', { sdpOffer: sdpOfferParam, 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 }, (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) + this.webRtcPeer.processAnswer(response.sdpAnswer) .then(() => { this.streamId = response.id; this.creationTime = response.createdAt; this.isLocalStreamPublished = true; this.publishedOnce = true; if (this.displayMyRemote()) { this.remotePeerSuccessfullyEstablished(); } this.ee.emitEvent('stream-created-by-publisher', []); this.initWebRtcStats(); resolve(); }) .catch(error => { reject(error); }); console.info("'Publisher' successfully published to session"); } }); }; if (this.displayMyRemote()) { this.webRtcPeer = new WebRtcPeerSendrecv(options); } else { this.webRtcPeer = new WebRtcPeerSendonly(options); } this.webRtcPeer.generateOffer().then(offer => { successCallback(offer); }).catch(error => { reject(new Error('(publish) SDP offer error: ' + JSON.stringify(error))); }); }); } private initWebRtcPeerReceive(): Promise { return new Promise((resolve, reject) => { const offerConstraints = { audio: this.inboundStreamOpts.hasAudio, video: this.inboundStreamOpts.hasVideo }; console.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) => { console.debug('Sending SDP offer to subscribe to ' + this.streamId, sdpOfferParam); this.session.openvidu.sendRequest('receiveVideoFrom', { sender: this.streamId, sdpOffer: sdpOfferParam }, (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 processAnswer method - if (this.session.isFirstIonicIosSubscriber) { - this.session.isFirstIonicIosSubscriber = false; - this.session['iosInterval'] = setTimeout(() => { - this.session.countDownForIonicIosSubscribers = false; - }, 400); - } - const needsTimeoutOnProcessAswer = this.session.countDownForIonicIosSubscribers; - this.webRtcPeer.processAnswer(response.sdpAnswer, needsTimeoutOnProcessAswer).then(() => { + this.webRtcPeer.processAnswer(response.sdpAnswer).then(() => { this.remotePeerSuccessfullyEstablished(); this.initWebRtcStats(); resolve(); }).catch(error => { reject(error); }); } }); }; this.webRtcPeer = new WebRtcPeerRecvonly(options); this.webRtcPeer.generateOffer() .then(offer => { successCallback(offer); }) .catch(error => { reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error))); }); }); } private remotePeerSuccessfullyEstablished(): void { if (platform['isIonicIos']) { // iOS Ionic. LIMITATION: must use deprecated WebRTC API const pc1: any = this.webRtcPeer.pc; this.mediaStream = pc1.getRemoteStreams()[0]; } else { this.mediaStream = new MediaStream(); let receiver: RTCRtpReceiver; for (receiver of this.webRtcPeer.pc.getReceivers()) { if (!!receiver.track) { this.mediaStream.addTrack(receiver.track); } } } console.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(); if (!this.displayMyRemote() && !!this.mediaStream.getAudioTracks()[0] && this.session.speakingEventsEnabled) { this.enableSpeakingEvents(); } } } private initWebRtcStats(): void { this.webRtcStats = new WebRtcStats(this); this.webRtcStats.initWebRtcStats(); //TODO: send common webrtc stats from client to openvidu-server /*if (this.session.openvidu.webrtcStatsInterval > 0) { setInterval(() => { this.gatherStatsForPeer().then(jsonStats => { const body = { sessionId: this.session.sessionId, participantPrivateId: this.connection.rpcSessionId, stats: jsonStats } var xhr = new XMLHttpRequest(); xhr.open('POST', this.session.openvidu.httpUri + '/elasticsearch/webrtc-stats', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(JSON.stringify(body)); }) }, this.session.openvidu.webrtcStatsInterval * 1000); }*/ } private stopWebRtcStats(): void { if (!!this.webRtcStats && this.webRtcStats.isEnabled()) { this.webRtcStats.stopWebRtcStats(); } } private getIceServersConf(): RTCIceServer[] | undefined { let returnValue; if (!!this.session.openvidu.advancedConfiguration.iceServers) { returnValue = this.session.openvidu.advancedConfiguration.iceServers === 'freeice' ? undefined : this.session.openvidu.advancedConfiguration.iceServers; } else if (this.session.openvidu.iceServers) { returnValue = this.session.openvidu.iceServers; } else { returnValue = undefined; } return returnValue; } private gatherStatsForPeer(): Promise { return new Promise((resolve, reject) => { if (this.isLocal()) { // Publisher stream stats this.getRTCPeerConnection().getSenders().forEach(sender => sender.getStats() .then( response => { response.forEach(report => { if (this.isReportWanted(report)) { const finalReport = {}; finalReport['type'] = report.type; finalReport['timestamp'] = report.timestamp; finalReport['id'] = report.id; // Common to Chrome, Firefox and Safari if (report.type === 'outbound-rtp') { finalReport['ssrc'] = report.ssrc; finalReport['firCount'] = report.firCount; finalReport['pliCount'] = report.pliCount; finalReport['nackCount'] = report.nackCount; finalReport['qpSum'] = report.qpSum; // Set media type if (!!report.kind) { finalReport['mediaType'] = report.kind; } else if (!!report.mediaType) { finalReport['mediaType'] = report.mediaType; } else { // Safari does not have 'mediaType' defined for inbound-rtp. Must be inferred from 'id' field finalReport['mediaType'] = (report.id.indexOf('VideoStream') !== -1) ? 'video' : 'audio'; } if (finalReport['mediaType'] === 'video') { finalReport['framesEncoded'] = report.framesEncoded; } finalReport['packetsSent'] = report.packetsSent; finalReport['bytesSent'] = report.bytesSent; } // Only for Chrome and Safari if (report.type === 'candidate-pair' && report.totalRoundTripTime !== undefined) { // This is the final selected candidate pair finalReport['availableOutgoingBitrate'] = report.availableOutgoingBitrate; finalReport['rtt'] = report.currentRoundTripTime; finalReport['averageRtt'] = report.totalRoundTripTime / report.responsesReceived; } // Only for Firefox >= 66.0 if (report.type === 'remote-inbound-rtp' || report.type === 'remote-outbound-rtp') { } console.log(finalReport); } }); })); } else { // Subscriber stream stats this.getRTCPeerConnection().getReceivers().forEach(receiver => receiver.getStats() .then( response => { response.forEach(report => { if (this.isReportWanted(report)) { const finalReport = {}; finalReport['type'] = report.type; finalReport['timestamp'] = report.timestamp; finalReport['id'] = report.id; // Common to Chrome, Firefox and Safari if (report.type === 'inbound-rtp') { finalReport['ssrc'] = report.ssrc; finalReport['firCount'] = report.firCount; finalReport['pliCount'] = report.pliCount; finalReport['nackCount'] = report.nackCount; finalReport['qpSum'] = report.qpSum; // Set media type if (!!report.kind) { finalReport['mediaType'] = report.kind; } else if (!!report.mediaType) { finalReport['mediaType'] = report.mediaType; } else { // Safari does not have 'mediaType' defined for inbound-rtp. Must be inferred from 'id' field finalReport['mediaType'] = (report.id.indexOf('VideoStream') !== -1) ? 'video' : 'audio'; } if (finalReport['mediaType'] === 'video') { finalReport['framesDecoded'] = report.framesDecoded; } finalReport['packetsReceived'] = report.packetsReceived; finalReport['packetsLost'] = report.packetsLost; finalReport['jitter'] = report.jitter; finalReport['bytesReceived'] = report.bytesReceived; } // Only for Chrome and Safari if (report.type === 'candidate-pair' && report.totalRoundTripTime !== undefined) { // This is the final selected candidate pair finalReport['availableIncomingBitrate'] = report.availableIncomingBitrate; finalReport['rtt'] = report.currentRoundTripTime; finalReport['averageRtt'] = report.totalRoundTripTime / report.responsesReceived; } // Only for Firefox >= 66.0 if (report.type === 'remote-inbound-rtp' || report.type === 'remote-outbound-rtp') { } console.log(finalReport); } }) }) ) } }); } private isReportWanted(report: any): boolean { return report.type === 'inbound-rtp' && !this.isLocal() || report.type === 'outbound-rtp' && this.isLocal() || (report.type === 'candidate-pair' && report.nominated && report.bytesSent > 0); } } \ No newline at end of file diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts index dd80468c..e2b69f8f 100644 --- a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts +++ b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts @@ -1,308 +1,308 @@ /* * (C) Copyright 2017-2019 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 freeice = require('freeice'); import uuid = require('uuid'); import platform = require('platform'); export interface WebRtcPeerConfiguration { mediaConstraints: { audio: boolean, video: boolean }; simulcast: boolean; onicecandidate: (event) => void; iceServers: RTCIceServer[] | undefined; mediaStream?: MediaStream; mode?: 'sendonly' | 'recvonly' | 'sendrecv'; id?: string; } export class WebRtcPeer { pc: RTCPeerConnection; id: string; remoteCandidatesQueue: RTCIceCandidate[] = []; localCandidatesQueue: RTCIceCandidate[] = []; iceCandidateList: RTCIceCandidate[] = []; private candidategatheringdone = false; constructor(private configuration: WebRtcPeerConfiguration) { this.configuration.iceServers = (!!this.configuration.iceServers && this.configuration.iceServers.length > 0) ? this.configuration.iceServers : freeice(); this.pc = new RTCPeerConnection({ iceServers: this.configuration.iceServers }); this.id = !!configuration.id ? configuration.id : uuid.v4(); this.pc.onicecandidate = event => { if (!!event.candidate) { const candidate: RTCIceCandidate = event.candidate; if (candidate) { this.localCandidatesQueue.push({ candidate: candidate.candidate }); this.candidategatheringdone = false; this.configuration.onicecandidate(event.candidate); } else if (!this.candidategatheringdone) { this.candidategatheringdone = true; } } }; this.pc.onsignalingstatechange = () => { if (this.pc.signalingState === 'stable') { while (this.iceCandidateList.length > 0) { this.pc.addIceCandidate(this.iceCandidateList.shift()); } } }; this.start(); } /** * This function creates the RTCPeerConnection object taking into account the * properties received in the constructor. It starts the SDP negotiation * process: generates the SDP offer and invokes the onsdpoffer callback. This * callback is expected to send the SDP offer, in order to obtain an SDP * answer from another peer. */ start(): Promise { return new Promise((resolve, reject) => { if (this.pc.signalingState === 'closed') { reject('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue'); } if (!!this.configuration.mediaStream) { if (platform['isIonicIos']) { // iOS Ionic. LIMITATION: must use deprecated WebRTC API const pc2: any = this.pc; pc2.addStream(this.configuration.mediaStream); } else { for (const track of this.configuration.mediaStream.getTracks()) { this.pc.addTrack(track, this.configuration.mediaStream); } } resolve(); } }); } /** * This method frees the resources used by WebRtcPeer */ dispose(videoSourceIsMediaStreamTrack: boolean) { console.debug('Disposing WebRtcPeer'); try { if (this.pc) { if (this.pc.signalingState === 'closed') { return; } this.remoteCandidatesQueue = []; this.localCandidatesQueue = []; if (platform['isIonicIos']) { // iOS Ionic. LIMITATION: must use deprecated WebRTC API // Stop senders deprecated const pc1: any = this.pc; for (const sender of pc1.getLocalStreams()) { if (!videoSourceIsMediaStreamTrack) { (sender).stop(); } pc1.removeStream(sender); } // Stop receivers deprecated for (const receiver of pc1.getRemoteStreams()) { if (!!receiver.track) { (receiver).stop(); } } } else { // Stop senders for (const sender of this.pc.getSenders()) { if (!videoSourceIsMediaStreamTrack) { if (!!sender.track) { sender.track.stop(); } } this.pc.removeTrack(sender); } // Stop receivers for (const receiver of this.pc.getReceivers()) { if (!!receiver.track) { receiver.track.stop(); } } } this.pc.close(); } } catch (err) { console.warn('Exception disposing webrtc peer ' + err); } } /** * 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) }; console.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 => { console.debug('Created SDP offer'); return this.pc.setLocalDescription(offer); }) .then(() => { const localDescription = this.pc.localDescription; if (!!localDescription) { console.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 => { console.debug('Created SDP offer'); return this.pc.setLocalDescription(offer); }) .then(() => { const localDescription = this.pc.localDescription; if (!!localDescription) { console.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, needsTimeoutOnProcessAswer: boolean): Promise { + processAnswer(sdpAnswer: string): Promise { return new Promise((resolve, reject) => { const answer: RTCSessionDescriptionInit = { type: 'answer', sdp: sdpAnswer }; console.debug('SDP answer received, setting remote description'); if (this.pc.signalingState === 'closed') { reject('RTCPeerConnection is closed'); } - if (needsTimeoutOnProcessAswer && platform['isIonicIos']) { + if (platform['isIonicIos']) { setTimeout(() => { console.info('setRemoteDescription run after timout for iOS device'); this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error)); }, 250); } else { // Rest of platforms this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error)); } }); } /** * Callback function invoked when an ICE candidate is received */ addIceCandidate(iceCandidate: RTCIceCandidate): Promise { return new Promise((resolve, reject) => { console.debug('Remote ICE candidate received', iceCandidate); this.remoteCandidatesQueue.push(iceCandidate); switch (this.pc.signalingState) { case 'closed': reject(new Error('PeerConnection object is closed')); break; case 'stable': if (!!this.pc.remoteDescription) { this.pc.addIceCandidate(iceCandidate).then(() => resolve()).catch(error => reject(error)); } else { this.iceCandidateList.push(iceCandidate); resolve(); } break; default: this.iceCandidateList.push(iceCandidate); resolve(); } }); } } export class WebRtcPeerRecvonly extends WebRtcPeer { constructor(configuration: WebRtcPeerConfiguration) { configuration.mode = 'recvonly'; super(configuration); } } export class WebRtcPeerSendonly extends WebRtcPeer { constructor(configuration: WebRtcPeerConfiguration) { configuration.mode = 'sendonly'; super(configuration); } } export class WebRtcPeerSendrecv extends WebRtcPeer { constructor(configuration: WebRtcPeerConfiguration) { configuration.mode = 'sendrecv'; super(configuration); } } \ No newline at end of file