Skip to content

TEL-6986: Generate silence toward A-leg from hold path - #574

Closed
minhtuan1407-telnyx wants to merge 1 commit into
telnyx/telephony/deploy-developmentfrom
jira-tel-6986
Closed

TEL-6986: Generate silence toward A-leg from hold path#574
minhtuan1407-telnyx wants to merge 1 commit into
telnyx/telephony/deploy-developmentfrom
jira-tel-6986

Conversation

@minhtuan1407-telnyx

@minhtuan1407-telnyx minhtuan1407-telnyx commented Apr 30, 2026

Copy link
Copy Markdown

Summary

Fixes TEL-6986 using the existing bridge variable:

bridge_generate_comfort_noise

When bridge_generate_comfort_noise is enabled and the bridge source leg is held, FreeSWITCH now generates silence RTP toward the non-held target leg. It still sends zero RTP toward the held/inactive leg.

This keeps the PR minimal by reusing FreeSWITCH's existing bridge-generated-comfort-noise knob instead of adding a new channel variable.

Existing variable reused

FreeSWITCH already has bridge-level generated silence/comfort-noise support:

bridge_generate_comfort_noise

Existing source behavior in src/switch_ivr_bridge.c:

  • reads bridge_generate_comfort_noise from chan_a,
  • parses true as default generated silence divisor 1400,
  • accepts numeric values,
  • initializes an L16 generated-silence frame,
  • uses that frame when bridge input is CNG / no-audio style media.

Existing docs/ChangeLog describes it as:

bridge_generate_comfort_noise option for bridge to generate comfort noise to the A leg when there is no audio on the B leg

TEL-6986 is the same class of problem, but the no-audio condition comes from SIP protocol hold / a=inactive rather than a normal CNG frame.

Problem

During B-leg protocol hold, FreeSWITCH correctly suppresses media toward the held/inactive B-leg. However, the bridge path can also stop sending RTP toward the non-held A-leg.

Failure mode before this fix:

B-leg sends a=inactive
FS -> held B: 0 RTP      # correct
FS -> non-held A: ~0 RTP # wrong; only transition packets

Prior log-based attempts using hold music / broadcast / silence_stream://-1 were insufficient because playback could execute while the PCAP still showed no sustained FS -> A RTP.

Final implementation

1. Reuse existing generated-silence frame setup

The PR keeps the existing bridge_generate_comfort_noise setup path and only tracks codec ownership with:

silence_codec_initialized

Cleanup now destroys the codec only if it was actually initialized.

2. Add a small helper for forced hold-generated silence writes

write_hold_generated_silence(...)

This helper:

  1. fills the prepared silence frame with switch_generate_sln_silence(), and
  2. writes it to the bridge target using SWITCH_IO_FLAG_FORCE.

3. Use directional hold predicates

Each bridge loop computes:

source_held = CF_HOLD(chan_a) || CF_LEG_HOLDING(chan_a)
target_held = CF_HOLD(chan_b) || CF_LEG_HOLDING(chan_b)

This is required because SIP/SDP protocol hold can set CF_LEG_HOLDING without using only the local/API CF_HOLD path.

4. Generate only from held source to non-held target

Generated silence is written only when:

silence_val && source_held && !target_held

That means:

  • B held, A not held: generate RTP toward A.
  • Target held: do not write RTP toward the held leg.

The same guard is present in both the suspend/hold path and the normal post-read bridge path, because hold can enter the suspended branch before the normal write branch.

5. Do not use normal writes while either side is held

The normal bridge write path is guarded by:

!source_held && !target_held

This avoids forwarding normal media into held endpoints. Hold-generated media is handled by the explicit generated-silence path above.

6. Force only the guarded generated-silence write through the media-flow gate

The retest showed the target A-leg can have target audio flow set to inactive while B is on hold (SWITCH_MEDIA_FLOW_INACTIVE, enum value 3). switch_core_media_write_frame() normally returns success without sending RTP when target audio flow is not SENDRECV or SENDONLY.

To avoid generated silence being silently dropped, the hold-generated-silence helper writes with:

SWITCH_IO_FLAG_FORCE

and the audio media-flow gate now preserves normal behavior except when FORCE is explicitly set:

if (!(flags & SWITCH_IO_FLAG_FORCE) &&
    audio_flow != SWITCH_MEDIA_FLOW_SENDRECV &&
    audio_flow != SWITCH_MEDIA_FLOW_SENDONLY) {
    return SWITCH_STATUS_SUCCESS;
}

Normal writes still respect media-flow direction. Only the guarded generated-silence write can bypass this gate.

Why not send_silence_when_idle?

HWR checked the existing variables thoroughly.

send_silence_when_idle is an IVR/session idle-loop variable used by sleep, park, digit collection, and SRTP idle-gap handling. It writes silence to the current idle session and does not implement bridge-direction hold behavior.

TEL-6986 requires bridge-specific asymmetric behavior:

source held + target not held => generated RTP to target
held target => zero RTP

So send_silence_when_idle is not the right knob.

Safety / scope

  • Reuses existing bridge_generate_comfort_noise; no new channel variable.
  • Only active when generated bridge comfort noise is already enabled.
  • Only writes when the source leg is held and the target leg is not held.
  • Does not send RTP toward the held/inactive leg after the inactive answer.
  • Normal media-flow behavior remains unchanged for non-forced writes.
  • No new logging was added.

Validation evidence

The TEL-6986 behavior was validated on the prior patch iteration with the same bridge hold write mechanics using an explicit temporary opt-in variable. HWR then re-reviewed the source and switched the opt-in to the existing bridge_generate_comfort_noise variable to minimize the PR surface.

The exact current head should be retested with:

bridge_generate_comfort_noise=true

Expected behavior is unchanged because the current source uses the same prepared generated-silence frame and the same guarded forced write path:

silence_val && source_held && !target_held

Latest validation artifacts from prior equivalent patch iteration

  • PCAP: TEL-6986.pcap
    • SHA256: 24a0c79225916f62ea1f11a0ee721ef26ebe258a0748c49d06f35df6f281672a
    • Packets: 2900
    • Duration: 18.283666s
  • Log: TEL-6986.log
    • SHA256: 15758cf0336c742cc9e3eb462c3e3c4a30db110a14f211f25326ef2406b00e15

SIP / SDP window:

  • B-leg hold offer: frame 1015, t=7.156475, SDP a=inactive.
  • FS hold answer: frame 1051, t=7.423097, SDP a=inactive.
  • B-leg unhold offer: frame 1817, t=12.533111, SDP a=sendrecv.
  • FS unhold answer: frame 1846, t=12.801459, SDP a=sendrecv.

Strict confirmed-hold window:

7.423097 <= t < 12.533111

RTP counts from that prior equivalent patch iteration:

A -> FS: 255
B -> FS: 256
FS -> A: 252
FS -> B: 0

FS -> A continuity:

  • Direction: 50.114.144.37:29014 -> 171.240.253.53:4022
  • Count: 252 packets.
  • Payload: PT0 / PCMU.
  • First packet: frame 1053, t=7.443045, seq 5424, RTP timestamp 43040.
  • Last packet: frame 1814, t=12.519906, seq 5675, RTP timestamp 83680.
  • Delta min/mean/max: 20.076ms / 20.227ms / 40.183ms.
  • Gaps >100ms: 0.

Result

The implementation keeps the required TEL-6986 behavior while using FreeSWITCH's existing bridge-generated comfort-noise variable:

  • no new channel variable,
  • generated silence while source leg is held,
  • explicit guard to keep RTP away from held targets,
  • FORCE only for generated silence so inactive target media flow does not silently suppress it,
  • normal media behavior preserved for all non-forced writes.

@minhtuan1407-telnyx
minhtuan1407-telnyx force-pushed the jira-tel-6986 branch 3 times, most recently from f0123b1 to 7f9ba95 Compare April 30, 2026 15:16
@minhtuan1407-telnyx

Copy link
Copy Markdown
Author

Superseded by Option A in #576. The new PR fixes TEL-6986 by preventing B-leg a=inactive from muting the non-held partner leg, without using bridge_generate_comfort_noise.

— 🪽 Hermes War Room

@minhtuan1407-telnyx

Copy link
Copy Markdown
Author

Closing as superseded by #576 per Option A direction.

— 🪽 Hermes War Room

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant