update lcc_dtmf to use media server

backport of https://github.com/jambonz/feature-server/pull/153
This commit is contained in:
Sam Machin
2026-07-29 15:51:14 +01:00
parent b356e893c0
commit de0fd932c8
2 changed files with 48 additions and 9 deletions
+41 -9
View File
@@ -458,6 +458,18 @@ class CallSession extends Emitter {
return this.accountInfo?.account?.service_provider_sid;
}
/**
* true when we are no longer anchored in the media path (media released to the SBC).
* Media release is a feature of the dial verb; when the current task is a dial we consult its
* media path, otherwise media is anchored at the media server for the life of the app.
*/
get isMediaReleased() {
if (this.currentTask?.name === TaskName.Dial) {
return this.currentTask.mediaPath !== MediaPath.FullMedia;
}
return false;
}
/**
* returns true if this session was transferred from another server
*/
@@ -1784,26 +1796,46 @@ class CallSession extends Emitter {
* media stream (via send_dtmf), which the SBC/rtpengine simply forwards. This is reliable
* regardless of whether rtpengine is transcoding the call. Sending SIP INFO to the SBC
* instead relies on rtpengine 'play DTMF' injection, which silently no-ops on calls that
* are not being transcoded (rtpengine has no DSP to synthesize the tone into). We fall back
* to SIP INFO only when we have no endpoint in the media path for the targeted leg, e.g. when
* media has been released to the SBC.
* are not being transcoded (rtpengine has no DSP to synthesize the tone into).
* this.ep is the endpoint facing the caller (A) leg; this.currentTask.ep is the endpoint
* facing the dialed (B) leg.
*
* We choose the path based on the media routing (this.isMediaReleased), not merely on whether
* an endpoint object exists. While anchored in the media path we send via send_dtmf, and if
* that fails we deliberately do NOT fall back to SIP INFO: the media path is the reliable one,
* so a failure here is a real error (falling back risks a silently-dropped or duplicated
* digit). We log and raise an alert instead. SIP INFO is used only once media has been
* released to the SBC, when there is no endpoint in the media path at all.
*/
const ep = callSid === this.callSid ? this.ep : this.currentTask?.ep;
if (ep?.connected) {
if (!this.isMediaReleased) {
const ep = callSid === this.callSid ? this.ep : this.currentTask?.ep;
try {
if (!ep?.connected) throw new Error('no connected media endpoint for the targeted leg');
this.logger.debug(`CallSession:_lccDtmf - sending ${digit}@${duration} via send_dtmf on ${ep.uuid}`);
await ep.execute('send_dtmf', `${digit}@${duration}`);
return;
} catch (err) {
this.logger.error({err},
'CallSession:_lccDtmf - error sending DTMF via send_dtmf, falling back to SIP INFO');
this.logger.error({err}, 'CallSession:_lccDtmf - error sending DTMF via send_dtmf');
const {writeAlerts} = this.srf.locals;
if (writeAlerts) {
try {
writeAlerts({
alert_type: 'dtmf-send-failure',
account_sid: this.accountSid,
service_provider_sid: this.serviceProviderSid,
message: `failed to send DTMF digit ${digit} to ${callSid === this.callSid ? 'caller' : 'called party'}`,
target_sid: this.callSid
});
} catch (alertErr) {
this.logger.error({err: alertErr}, 'CallSession:_lccDtmf - error writing dtmf-send-failure alert');
}
}
return;
}
}
/* fallback: signal the SBC to inject DTMF via SIP INFO (used when media is released
* and we have no endpoint in the media path for the targeted leg)
/* no endpoint in the media path (e.g. media released to the SBC): signal the SBC to inject
* DTMF via SIP INFO
*/
try {
const dlg = callSid === this.callSid ? this.dlg : this.currentTask.dlg;
+7
View File
@@ -176,6 +176,13 @@ class TaskDial extends Task {
return this.data.exitMediaPath;
}
/* current media routing for this dial: MediaPath.FullMedia (anchored at the media server),
* PartialMedia or NoMedia (media released to the SBC)
*/
get mediaPath() {
return this._mediaPath;
}
get summary() {
if (this.target.length === 1) {
const target = this.target[0];