diff --git a/lib/tasks/gather.js b/lib/tasks/gather.js index 89f73624..9cff960c 100644 --- a/lib/tasks/gather.js +++ b/lib/tasks/gather.js @@ -910,11 +910,7 @@ class TaskGather extends SttTask { const bugname = fsEvent.getHeader('media-bugname'); const finished = fsEvent.getHeader('transcription-session-finished'); this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription raw transcript'); - if (bugname && this.bugname !== bugname) { - this.logger.info( - `Gather:_onTranscription - ignoring transcript from ${bugname} because our bug is ${this.bugname}`); - return; - } + if (!this.eventIsForOurBug(fsEvent)) return; if (finished === 'true') return; if (this.vendor === 'ibm' && evt?.state === 'listening') return; diff --git a/lib/tasks/listen.js b/lib/tasks/listen.js index 6dc1070e..921a1255 100644 --- a/lib/tasks/listen.js +++ b/lib/tasks/listen.js @@ -67,6 +67,7 @@ class TaskListen extends Task { get name() { return TaskName.Listen; } set bugname(name) { this._bugname = name; } + get bugname() { return this._bugname; } set ignoreCustomerData(val) { this._ignoreCustomerData = val; } @@ -192,32 +193,31 @@ class TaskListen extends Task { } _initListeners(ep) { - ep.addCustomEventListener(ListenEvents.Connect, this._onConnect.bind(this, ep)); - ep.addCustomEventListener(ListenEvents.ConnectFailure, this._onConnectFailure.bind(this, ep)); - ep.addCustomEventListener(ListenEvents.Error, this._onError.bind(this, ep)); + /* Register via the base-class addCustomEventListener so handlers are tracked + * and removed by reference. Multiple forks (e.g. this listen verb and a + * background recording fork) share one endpoint and subscribe to the same + * event names; removing by reference avoids tearing down the co-tenant + * task's handlers (removing with no handler would call removeAllListeners + * and unsubscribe the event for everyone). */ + this.addCustomEventListener(ep, ListenEvents.Connect, this._onConnect.bind(this, ep)); + this.addCustomEventListener(ep, ListenEvents.ConnectFailure, this._onConnectFailure.bind(this, ep)); + this.addCustomEventListener(ep, ListenEvents.Error, this._onError.bind(this, ep)); + this.addCustomEventListener(ep, ListenEvents.KillAudio, this._onKillAudio.bind(this, ep)); + this.addCustomEventListener(ep, ListenEvents.Disconnect, this._onDisconnect.bind(this, ep)); + /* support bi-directional audio */ + if (this.bidirectionalAudio.enabled) { + this.addCustomEventListener(ep, ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep)); + } if (this.finishOnKey || this.passDtmf) { ep.on('dtmf', this._dtmfHandler); } - - /* support bi-directional audio */ - if (this.bidirectionalAudio.enabled) { - ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep)); - } - ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep)); - ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep)); } _removeListeners(ep) { - ep.removeCustomEventListener(ListenEvents.Connect); - ep.removeCustomEventListener(ListenEvents.ConnectFailure); - ep.removeCustomEventListener(ListenEvents.Error); + this.removeCustomEventListeners(ep); if (this.finishOnKey || this.passDtmf) { ep.removeListener('dtmf', this._dtmfHandler); } - ep.removeCustomEventListener(ListenEvents.PlayAudio); - ep.removeCustomEventListener(ListenEvents.KillAudio); - ep.removeCustomEventListener(ListenEvents.Disconnect); - } _onDtmf(ep, evt) { @@ -253,10 +253,12 @@ class TaskListen extends Task { this._timer = null; } } - _onConnect(ep) { + _onConnect(ep, evt, fsEvent) { + if (!this.eventIsForOurBug(fsEvent)) return; this.logger.info('TaskListen:_onConnect'); } - _onConnectFailure(ep, evt) { + _onConnectFailure(ep, evt, fsEvent) { + if (!this.eventIsForOurBug(fsEvent)) return; this.logger.info(evt, 'TaskListen:_onConnectFailure'); this.notifyTaskDone(); } @@ -279,7 +281,8 @@ class TaskListen extends Task { } } - async _onPlayAudio(ep, evt) { + async _onPlayAudio(ep, evt, fsEvent) { + if (!this.eventIsForOurBug(fsEvent)) return; this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`); if (!evt.queuePlay) { this.playAudioQueue = []; @@ -301,17 +304,20 @@ class TaskListen extends Task { this.isPlayingAudioFromQueue = false; } - _onKillAudio(ep) { + _onKillAudio(ep, evt, fsEvent) { + if (!this.eventIsForOurBug(fsEvent)) return; this.logger.info('received kill_audio event'); ep.api('uuid_break', ep.uuid); } - _onDisconnect(ep, cs) { + _onDisconnect(ep, evt, fsEvent) { + if (!this.eventIsForOurBug(fsEvent)) return; this.logger.debug('_onDisconnect: TaskListen terminating task'); - this.kill(cs); + this.kill(); } - _onError(ep, evt) { + _onError(ep, evt, fsEvent) { + if (!this.eventIsForOurBug(fsEvent)) return; this.logger.info(evt, 'TaskListen:_onError'); this.notifyTaskDone(); } diff --git a/lib/tasks/stt-task.js b/lib/tasks/stt-task.js index b51c3e12..a0e92245 100644 --- a/lib/tasks/stt-task.js +++ b/lib/tasks/stt-task.js @@ -51,7 +51,6 @@ class SttTask extends Task { this.compileSonioxTranscripts = compileSonioxTranscripts; this.consolidateTranscripts = consolidateTranscripts; this.updateSpeechmaticsPayload = updateSpeechmaticsPayload; - this.eventHandlers = []; this.isHandledByPrimaryProvider = true; /** * Task use taskIncludeRecognizer to identify @@ -245,26 +244,6 @@ class SttTask extends Task { return {host, path: `${pathname}${search}`}; } - addCustomEventListener(ep, event, handler) { - this.eventHandlers.push({ep, event, handler}); - ep.addCustomEventListener(event, handler); - } - - removeCustomEventListeners(ep) { - if (ep) { - // for specific endpoint - this.eventHandlers.filter((h) => h.ep === ep).forEach((h) => { - h.ep.removeCustomEventListener(h.event, h.handler); - }); - this.eventHandlers = this.eventHandlers.filter((h) => h.ep !== ep); - return; - } else { - // for all endpoints - this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler)); - this.eventHandlers = []; - } - } - async _initSpeechCredentials(cs, vendor, label) { const {getNuanceAccessToken, getIbmAccessToken, getAwsAuthToken, getVerbioAccessToken} = cs.srf.locals.dbHelpers; let credentials = cs.getSpeechCredentials(vendor, 'stt', label); diff --git a/lib/tasks/task.js b/lib/tasks/task.js index bedcf43e..9de60b89 100644 --- a/lib/tasks/task.js +++ b/lib/tasks/task.js @@ -24,6 +24,10 @@ class Task extends Emitter { this._killInProgress = false; this._completionPromise = new Promise((resolve) => this._completionResolver = resolve); + /* tracks {ep, event, handler} for custom event listeners so they can be + * removed by reference (see addCustomEventListener/removeCustomEventListeners) */ + this.eventHandlers = []; + /* used when we play a prompt to a member in conference */ this._confPlayCompletionPromise = new Promise((resolve) => this._confPlayCompletionResolver = resolve); } @@ -103,6 +107,67 @@ class Task extends Emitter { } } + /** + * Decide whether a FreeSWITCH media-bug custom event belongs to this task. + * + * Several FreeSWITCH modules (mod_audio_fork, mod_audio_stream, ...) fire the + * same custom event names for *every* media bug on a shared endpoint. When more + * than one bug runs on a call (e.g. a listen verb plus a background recording + * fork, or transcription plus answering-machine detection) each task must only + * act on events for its own bug, identified by the 'media-bugname' header. + * + * Fails open (returns true) when the header is absent or this task has no + * bugname, preserving single-bug behavior and tolerating older FreeSWITCH + * builds that don't yet stamp the header. + * + * @param {object} fsEvent - raw FreeSWITCH event (has getHeader) + * @returns {boolean} true if the event is for this task's bug + */ + eventIsForOurBug(fsEvent) { + const bugname = fsEvent?.getHeader?.('media-bugname'); + if (bugname && this.bugname && bugname !== this.bugname) { + this.logger.debug( + `${this.name}: ignoring media-bug event for ${bugname}, ours is ${this.bugname}`); + return false; + } + return true; + } + + /** + * Subscribe to a FreeSWITCH custom event on an endpoint, tracking the handler + * so it can later be removed by reference. Endpoints can be shared by several + * tasks (e.g. a listen verb and a background recording fork), so listeners must + * be removed individually — removing without a handler reference would call + * removeAllListeners and tear down co-tenant tasks' handlers. + * @param {object} ep - the endpoint + * @param {string} event - the custom event name + * @param {function} handler - the (already-bound) listener + */ + addCustomEventListener(ep, event, handler) { + this.eventHandlers.push({ep, event, handler}); + ep.addCustomEventListener(event, handler); + } + + /** + * Remove the custom event listeners this task registered. With an endpoint, + * removes only listeners for that endpoint; without one, removes all. + * @param {object} [ep] - the endpoint, or undefined for all endpoints + */ + removeCustomEventListeners(ep) { + if (ep) { + // for specific endpoint + this.eventHandlers.filter((h) => h.ep === ep).forEach((h) => { + h.ep.removeCustomEventListener(h.event, h.handler); + }); + this.eventHandlers = this.eventHandlers.filter((h) => h.ep !== ep); + return; + } else { + // for all endpoints + this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler)); + this.eventHandlers = []; + } + } + /** * when a subclass Task has completed its work, it should call this method */ diff --git a/lib/tasks/transcribe.js b/lib/tasks/transcribe.js index 00d65c1a..ab5a1845 100644 --- a/lib/tasks/transcribe.js +++ b/lib/tasks/transcribe.js @@ -488,10 +488,9 @@ class TaskTranscribe extends SttTask { ep.gracefulShutdownResolver(); } // make sure this is not a transcript from answering machine detection - const bugname = fsEvent.getHeader('media-bugname'); const finished = fsEvent.getHeader('transcription-session-finished'); const bufferedTranscripts = this._bufferedTranscripts[channel - 1]; - if (bugname && this.bugname !== bugname) return; + if (!this.eventIsForOurBug(fsEvent)) return; if (this.paused) { this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - paused, ignoring transcript'); }