mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-10-04 02:04:12 +00:00
feat: surface in-dialog hold/un-hold to the application (#1564)
Applications currently have no way to observe a mid-call hold/un-hold.
The feature-server handles the in-dialog re-INVITE internally (re-modifies
the endpoint, returns 200) but never notifies the app, so a transcription
or agent-assist app can't pause/resume when a caller is held or retrieved.
This detects the hold state transition on a re-INVITE and delivers it over
the existing sipRequestWithinDialogHook channel as a verb:hook, plus emits
a local 'hold'/'unhold' event so in-process tasks (e.g. transcribe) can react.
- call-session.js: ordinary calls — reuse isOnhold() (a=sendonly/a=inactive).
- siprec-call-session.js: SIPREC overrides _onReinvite, so a separate handler
is required. SIPREC recording streams are sendonly by nature, so a stream
flipping to a=inactive is treated as the hold signal; the full multipart
body (SDP + rs-metadata) is always forwarded. Fires on transition only;
other re-INVITEs are delivered as event:"reinvite".
Delivery uses requestor.request('verb:hook', ...) — the same channel
_handleRefer uses — rather than a task-bound performHook, so events are not
dropped if the current task has ended. Enable per call with:
{"verb":"config","sipRequestWithinDialogHook":"/hold-events"}
Confirmed live against Cisco CUBE (Cisco-SIPGateway/IOS-17.12.7b) over SIPREC:
both recording streams flip sendonly<->inactive on hold/resume, each
transition emitted correctly.
The whole feature is opt-in behind the JAMBONES_HOLD_UNHOLD_EVENTS env var;
when it is unset, _notifyHoldState / _notifySipRecReinvite are no-ops (neither
the local event nor the hook fires).
Reinstates the feature previously reverted in #1563. The revert was due to the
unit test failing in the full suite: lib/config.js captures the env var once at
load time, and earlier suite files require config/session modules (flag unset)
before this test could set it, so the enabled-path assertions saw a disabled
feature. The test now force-reloads config + the session modules with the flag
set (and restores the module cache afterward), so it is robust to suite load order.
This commit is contained in:
@@ -29,6 +29,7 @@ const {parseUri} = require('drachtio-srf');
|
||||
const {
|
||||
JAMBONES_INJECT_CONTENT,
|
||||
JAMBONES_EAGERLY_PRE_CACHE_AUDIO,
|
||||
JAMBONES_HOLD_UNHOLD_EVENTS,
|
||||
AWS_REGION,
|
||||
} = require('../config');
|
||||
const bent = require('bent');
|
||||
@@ -38,6 +39,7 @@ const BADPRECONDITIONS = 'preconditions not met';
|
||||
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
|
||||
const { NonFatalTaskError} = require('../utils/error');
|
||||
const { createMediaEndpoint } = require('../utils/media-endpoint');
|
||||
const { isOnhold } = require('../utils/sdp-utils');
|
||||
const SttLatencyCalculator = require('../utils/stt-latency-calculator');
|
||||
const sqlRetrieveQueueEventHook = `SELECT * FROM webhooks
|
||||
WHERE webhook_sid =
|
||||
@@ -2793,6 +2795,7 @@ Duration=${duration} `
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
|
||||
this._notifyHoldState(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2810,6 +2813,43 @@ Duration=${duration} `
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect a hold/un-hold transition on an in-dialog re-INVITE and notify the
|
||||
* application (reusing the sipRequestWithinDialogHook channel). Also emits a
|
||||
* local 'hold'/'unhold' event so in-process tasks (e.g. transcribe) can react.
|
||||
* Opt-in: only runs when JAMBONES_HOLD_UNHOLD_EVENTS is set.
|
||||
*/
|
||||
_notifyHoldState(req) {
|
||||
if (!JAMBONES_HOLD_UNHOLD_EVENTS) return;
|
||||
const onHold = isOnhold(req.body);
|
||||
const wasOnHold = !!this._onHold;
|
||||
if (onHold === wasOnHold) return; // no state change -> nothing to do
|
||||
this._onHold = onHold;
|
||||
const event = onHold ? 'hold' : 'unhold';
|
||||
this.logger.info({event, callSid: this.callSid}, 'CallSession: hold state changed');
|
||||
|
||||
// in-process signal (used by the optional transcribe auto-pause)
|
||||
this.emit(event);
|
||||
|
||||
// notify the application over the in-dialog SIP hook, if configured.
|
||||
// Delivered via requestor.request('verb:hook', ...) — the same channel
|
||||
// _handleRefer uses — rather than currentTask.performHook, so the event is
|
||||
// not dropped when the current task has ended (e.g. transcribe failed).
|
||||
// callInfo.toJSON() is merged in manually so call_sid/account_sid are present.
|
||||
if (this.sipRequestWithinDialogHook && this.requestor) {
|
||||
const params = {
|
||||
...(this.callInfo.toJSON()),
|
||||
sip_method: 'INVITE',
|
||||
event, // 'hold' | 'unhold'
|
||||
on_hold: onHold,
|
||||
sip_body: req.body,
|
||||
sip_headers: req.headers
|
||||
};
|
||||
this.requestor.request('verb:hook', this.sipRequestWithinDialogHook, params)
|
||||
.catch((err) => this.logger.error({err}, 'CallSession:_notifyHoldState - hook error'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming REFER
|
||||
* @param {*} req
|
||||
|
||||
Reference in New Issue
Block a user