diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index 8f8ebf5b..b4ab17c9 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -100,7 +100,19 @@ export class Session implements EventDispatcher { /** * @hidden */ - speakingEventsEnabled = false; + startSpeakingEventsEnabled = false; + /** + * @hidden + */ + startSpeakingEventsEnabledOnce = false; + /** + * @hidden + */ + stopSpeakingEventsEnabled = false; + /** + * @hidden + */ + stopSpeakingEventsEnabledOnce = false; private ee = new EventEmitter(); @@ -569,13 +581,23 @@ export class Session implements EventDispatcher { handler(event); }); - if (type === 'publisherStartSpeaking' || type === 'publisherStopSpeaking') { - this.speakingEventsEnabled = true; + if (type === 'publisherStartSpeaking') { + this.startSpeakingEventsEnabled = 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(); + str.enableStartSpeakingEvent(); + } + } + } + if (type === 'publisherStopSpeaking') { + this.stopSpeakingEventsEnabled = true; + // If there are already available remote streams, enable hark 'stopped_speaking' event in all of them + for (const connectionId in this.remoteConnections) { + const str = this.remoteConnections[connectionId].stream; + if (!!str && str.hasAudio) { + str.enableStopSpeakingEvent(); } } } @@ -591,20 +613,30 @@ export class Session implements EventDispatcher { this.ee.once(type, event => { if (event) { - console.info("Event '" + type + "' triggered by 'Session'", event); + console.info("Event '" + type + "' triggered once by 'Session'", event); } else { - console.info("Event '" + type + "' triggered by 'Session'"); + console.info("Event '" + type + "' triggered once 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 + if (type === 'publisherStartSpeaking') { + this.startSpeakingEventsEnabledOnce = true; + // If there are already available remote streams, enable hark 'speaking' event in all of them once for (const connectionId in this.remoteConnections) { const str = this.remoteConnections[connectionId].stream; if (!!str && str.hasAudio) { - str.enableOnceSpeakingEvents(); + str.enableOnceStartSpeakingEvent(); + } + } + } + if (type === 'publisherStopSpeaking') { + this.stopSpeakingEventsEnabledOnce = true; + // If there are already available remote streams, enable hark 'stopped_speaking' event in all of them once + for (const connectionId in this.remoteConnections) { + const str = this.remoteConnections[connectionId].stream; + if (!!str && str.hasAudio) { + str.enableOnceStopSpeakingEvent(); } } } @@ -617,21 +649,35 @@ export class Session implements EventDispatcher { * 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(); + if (type === 'publisherStartSpeaking') { + let remainingStartSpeakingListeners = this.ee.getListeners(type).length; + if (remainingStartSpeakingListeners === 0) { + this.startSpeakingEventsEnabled = 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.disableStartSpeakingEvent(false); + } + } + } + } + if (type === 'publisherStopSpeaking') { + let remainingStopSpeakingListeners = this.ee.getListeners(type).length; + if (remainingStopSpeakingListeners === 0) { + this.stopSpeakingEventsEnabled = 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.disableStopSpeakingEvent(false); + } } } } diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index 4fd423eb..87930dd1 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -168,14 +168,26 @@ export class Stream implements EventDispatcher { * @hidden */ publisherStartSpeakingEventEnabled = false; + /** + * @hidden + */ + publisherStartSpeakingEventEnabledOnce = false; /** * @hidden */ publisherStopSpeakingEventEnabled = false; + /** + * @hidden + */ + publisherStopSpeakingEventEnabledOnce = false; /** * @hidden */ volumeChangeEventEnabled = false; + /** + * @hidden + */ + volumeChangeEventEnabledOnce = false; /** @@ -529,30 +541,73 @@ export class Stream implements EventDispatcher { /** * @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 { + enableStartSpeakingEvent(): 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)]); + this.publisherStartSpeakingEventEnabledOnce = false; // Disable 'once' version if 'on' version was triggered }); } + } + + /** + * @hidden + */ + enableOnceStartSpeakingEvent(): void { + this.setSpeechEventIfNotExists(); + if (!this.publisherStartSpeakingEventEnabledOnce) { + this.publisherStartSpeakingEventEnabledOnce = true; + this.speechEvent.once('speaking', () => { + if (this.publisherStartSpeakingEventEnabledOnce) { + // If the listener has been disabled in the meantime (for example by the 'on' version) do not trigger the event + this.session.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStartSpeaking', this.connection, this.streamId)]); + } + this.disableStartSpeakingEvent(true); + }); + } + } + + /** + * @hidden + */ + disableStartSpeakingEvent(disabledByOnce: boolean): void { + if (!!this.speechEvent) { + this.publisherStartSpeakingEventEnabledOnce = false; + if (disabledByOnce) { + if (this.publisherStartSpeakingEventEnabled) { + // The 'on' version of this same event is enabled too. Do not remove the hark listener + return; + } + } else { + this.publisherStartSpeakingEventEnabled = false; + } + // Shutting down the hark event + if (this.volumeChangeEventEnabled || + this.volumeChangeEventEnabledOnce || + this.publisherStopSpeakingEventEnabled || + this.publisherStopSpeakingEventEnabledOnce) { + // Some other hark event is enabled. Cannot stop the hark process, just remove the specific listener + this.speechEvent.off('speaking'); + } else { + // No other hark event is enabled. We can get entirely rid of it + this.speechEvent.stop(); + delete this.speechEvent; + } + } + } + + /** + * @hidden + */ + enableStopSpeakingEvent(): void { + this.setSpeechEventIfNotExists(); if (!this.publisherStopSpeakingEventEnabled) { this.publisherStopSpeakingEventEnabled = true; this.speechEvent.on('stopped_speaking', () => { this.session.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId)]); + this.publisherStopSpeakingEventEnabledOnce = false; // Disable 'once' version if 'on' version was triggered }); } } @@ -560,89 +615,119 @@ export class Stream implements EventDispatcher { /** * @hidden */ - enableOnceSpeakingEvents(): void { + enableOnceStopSpeakingEvent(): 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; + if (!this.publisherStopSpeakingEventEnabledOnce) { + this.publisherStopSpeakingEventEnabledOnce = true; this.speechEvent.once('stopped_speaking', () => { - this.session.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId)]); - this.disableSpeakingEvents(); + if (this.publisherStopSpeakingEventEnabledOnce) { + // If the listener has been disabled in the meantime (for example by the 'on' version) do not trigger the event + this.session.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId)]); + } + this.disableStopSpeakingEvent(true); }); } } /** - * @hidden - */ - disableSpeakingEvents(): void { + * @hidden + */ + disableStopSpeakingEvent(disabledByOnce: boolean): void { if (!!this.speechEvent) { - if (this.volumeChangeEventEnabled) { - // 'streamAudioVolumeChange' event is enabled. Cannot stop the hark process - this.speechEvent.off('speaking'); + this.publisherStopSpeakingEventEnabledOnce = false; + if (disabledByOnce) { + if (this.publisherStopSpeakingEventEnabled) { + // We are cancelling the 'once' listener for this event, but the 'on' version + // of this same event is enabled too. Do not remove the hark listener + return; + } + } else { + this.publisherStopSpeakingEventEnabled = false; + } + // Shutting down the hark event + if (this.volumeChangeEventEnabled || + this.volumeChangeEventEnabledOnce || + this.publisherStartSpeakingEventEnabled || + this.publisherStartSpeakingEventEnabledOnce) { + // Some other hark event is enabled. Cannot stop the hark process, just remove the specific listener this.speechEvent.off('stopped_speaking'); } else { + // No other hark event is enabled. We can get entirely rid of it this.speechEvent.stop(); delete this.speechEvent; } } - this.publisherStartSpeakingEventEnabled = false; - this.publisherStopSpeakingEventEnabled = false; } /** * @hidden */ - enableVolumeChangeEvent(): void { - this.setSpeechEventIfNotExists(); - if (!this.volumeChangeEventEnabled) { + enableVolumeChangeEvent(force: boolean): void { + if (this.setSpeechEventIfNotExists()) { + if (!this.volumeChangeEventEnabled || force) { + 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)]); + }); + } + } else { + // This way whenever the MediaStream object is available, the event listener will be automatically added 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)]); - }); + enableOnceVolumeChangeEvent(force: boolean): void { + if (this.setSpeechEventIfNotExists()) { + if (!this.volumeChangeEventEnabledOnce || force) { + this.volumeChangeEventEnabledOnce = true; + this.speechEvent.once('volume_change', harkEvent => { + const oldValue = this.speechEvent.oldVolumeValue; + const value = { newValue: harkEvent, oldValue }; + this.speechEvent.oldVolumeValue = harkEvent; + this.disableVolumeChangeEvent(true); + this.streamManager.emitEvent('streamAudioVolumeChange', [new StreamManagerEvent(this.streamManager, 'streamAudioVolumeChange', value)]); + }); + } + } else { + // This way whenever the MediaStream object is available, the event listener will be automatically added + this.volumeChangeEventEnabledOnce = true; } } /** * @hidden */ - disableVolumeChangeEvent(): void { + disableVolumeChangeEvent(disabledByOnce: boolean): void { if (!!this.speechEvent) { - if (this.session.speakingEventsEnabled) { - // 'publisherStartSpeaking' and/or publisherStopSpeaking` events are enabled. Cannot stop the hark process + this.volumeChangeEventEnabledOnce = false; + if (disabledByOnce) { + if (this.volumeChangeEventEnabled) { + // We are cancelling the 'once' listener for this event, but the 'on' version + // of this same event is enabled too. Do not remove the hark listener + return; + } + } else { + this.volumeChangeEventEnabled = false; + } + // Shutting down the hark event + if (this.publisherStartSpeakingEventEnabled || + this.publisherStartSpeakingEventEnabledOnce || + this.publisherStopSpeakingEventEnabled || + this.publisherStopSpeakingEventEnabledOnce) { + // Some other hark event is enabled. Cannot stop the hark process, just remove the specific listener this.speechEvent.off('volume_change'); } else { + // No other hark event is enabled. We can get entirely rid of it this.speechEvent.stop(); delete this.speechEvent; } } - this.volumeChangeEventEnabled = false; } /** @@ -680,9 +765,24 @@ export class Stream implements EventDispatcher { /* Private methods */ + private setSpeechEventIfNotExists(): boolean { + if (!!this.mediaStream) { + 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); + } + return true; + } + return false; + } + private initWebRtcPeerSend(): Promise { return new Promise((resolve, reject) => { + this.initHarkEvents(); // Init hark events for the local stream + const userMediaConstraints = { audio: this.isSendAudio(), video: this.isSendVideo() @@ -849,8 +949,34 @@ export class Stream implements EventDispatcher { } this.updateMediaStreamInVideos(); - if (!this.displayMyRemote() && !!this.mediaStream.getAudioTracks()[0] && this.session.speakingEventsEnabled) { - this.enableSpeakingEvents(); + this.initHarkEvents(); // Init hark events for the remote stream + } + } + + private initHarkEvents(): void { + if (!!this.mediaStream.getAudioTracks()[0]) { + // Hark events can only be set if audio track is available + if (this.streamManager.remote) { + // publisherStartSpeaking/publisherStopSpeaking is only defined for remote streams + if (this.session.startSpeakingEventsEnabled) { + this.enableStartSpeakingEvent(); + } + if (this.session.startSpeakingEventsEnabledOnce) { + this.enableOnceStartSpeakingEvent(); + } + if (this.session.stopSpeakingEventsEnabled) { + this.enableStopSpeakingEvent(); + } + if (this.session.stopSpeakingEventsEnabledOnce) { + this.enableOnceStopSpeakingEvent(); + } + } + // streamAudioVolumeChange event is defined for both Publishers and Subscribers + if (this.volumeChangeEventEnabled) { + this.enableVolumeChangeEvent(true); + } + if (this.volumeChangeEventEnabledOnce) { + this.enableOnceVolumeChangeEvent(true); } } } diff --git a/openvidu-browser/src/OpenVidu/StreamManager.ts b/openvidu-browser/src/OpenVidu/StreamManager.ts index c3e296ff..618c3eab 100644 --- a/openvidu-browser/src/OpenVidu/StreamManager.ts +++ b/openvidu-browser/src/OpenVidu/StreamManager.ts @@ -167,7 +167,7 @@ export class StreamManager implements EventDispatcher { } } if (type === 'streamAudioVolumeChange' && this.stream.hasAudio) { - this.stream.enableVolumeChangeEvent(); + this.stream.enableVolumeChangeEvent(false); } return this; } @@ -178,9 +178,9 @@ export class StreamManager implements EventDispatcher { once(type: string, handler: (event: Event) => void): StreamManager { this.ee.once(type, event => { if (event) { - console.info("Event '" + type + "' triggered once", event); + console.info("Event '" + type + "' triggered once by '" + (this.remote ? 'Subscriber' : 'Publisher') + "'", event); } else { - console.info("Event '" + type + "' triggered once"); + console.info("Event '" + type + "' triggered once by '" + (this.remote ? 'Subscriber' : 'Publisher') + "'"); } handler(event); }); @@ -200,7 +200,7 @@ export class StreamManager implements EventDispatcher { } } if (type === 'streamAudioVolumeChange' && this.stream.hasAudio) { - this.stream.enableOnceVolumeChangeEvent(); + this.stream.enableOnceVolumeChangeEvent(false); } return this; } @@ -216,7 +216,10 @@ export class StreamManager implements EventDispatcher { } if (type === 'streamAudioVolumeChange') { - this.stream.disableVolumeChangeEvent(); + let remainingVolumeEventListeners = this.ee.getListeners(type).length; + if (remainingVolumeEventListeners === 0) { + this.stream.disableVolumeChangeEvent(false); + } } return this;