Skip to content

Raise CallRinging when an outbound call starts ringing - #28

Merged
calebtt merged 1 commit into
masterfrom
feature/ringing-event
Sep 26, 2026
Merged

calebtt merged 1 commit into
masterfrom
feature/ringing-event

Conversation

@calebtt

@calebtt calebtt commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #27. This is the v0.1.5 change (PR-DIAL-1 in the pbx-voice requirements).

SipClient reported the first ringing response only as StatusMessage text, and CallAsync's ring timeout starts when the INVITE is sent. A host that wants to time the ring from when the phone actually rings had no reliable signal. The pbx-voice daemon needs one: it measures ring time from the first 180 or 183, and treats "no ringing within 7 s of the INVITE" as a setup failure.

Change

  • New public event Action<SipClient, int>? CallRinging. It is raised once per CallAsync call, on the first 180 Ringing or 183 Session Progress, with that status code. Those are exactly the responses SIPSorcery routes to ClientCallRinging; other provisional responses go to ClientCallTrying.
  • It is not raised when no ringing response arrives, and never twice for one call. The once-per-call flag resets at the start of each CallAsync.
  • StatusMessage ("Call ringing: 180 Ringing.") and all other events are unchanged. The private handler was renamed from CallRinging to OnClientCallRinging to free the name.

Intended use: pass a large ringTimeoutSeconds to CallAsync, start your own timer on CallRinging, and cancel through the CancellationToken.

Tests

New CallRingingEventTests (the plan's D-U1). A scripted loopback callee answers each INVITE with the given provisional responses, then 486, so no media is needed:

Callee sends Event
180 once, 180
183 once, 183
180, 180 once, 180
183, 180 once, 183
181 only none
no provisional response none

It also fires again for the next call, and StatusMessage still reports ringing.

dotnet test: 106 passed (98 + 8). The new tests passed 3 of 3 repeated runs. Build warnings unchanged (17).

Compatibility

Additive only. SipBotOpen and homeline compile unchanged, and sipbot serve's JSONL does not change. Live checks against the lab PBX (D-L1 to D-L3) follow before tagging v0.1.5.

🤖 Generated with Claude Code

SipClient reported the first ringing response only as StatusMessage text,
and CallAsync's ring timeout starts when the INVITE is sent, so digest
challenges, PBX routing, and carrier setup all counted against the ring
time. A host that wants to time the ring from when the phone actually rings
had no reliable signal.

SipClient.CallRinging is raised once per CallAsync call, on the first
180 Ringing or 183 Session Progress (exactly what SIPSorcery routes to
ClientCallRinging), with that status code. It is not raised without a
ringing response and never twice for one call. StatusMessage and the other
events are unchanged; the private handler was renamed to free the name.

Tests: a scripted loopback callee sends 180, 183, 180 twice, 183 then 180,
181 only, or nothing, then 486. The event fires once with the first 180 or
183, never for 181 or no provisional response, resets for the next call,
and StatusMessage still reports ringing.

Closes #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@calebtt

calebtt commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Live check

Tested against a VitalPBX (Asterisk 20.14) lab PBX, extension to extension. The caller behaved like the intended host: CallAsync with a 60 s cap, a 7 s timer started on CallRinging, and a cancel through the CancellationToken. A scripted SIPSorcery callee answered each INVITE with 180, with 183, or with nothing.

Callee sends CallRinging INVITE → event Event → CANCEL Callee got the CANCEL
180 once, 180 328 ms 7004 ms yes (200 to the CANCEL, 487 to the INVITE)
183 once, 180 380 ms 7012 ms yes
nothing once, 180 258 ms 7006 ms no (see below)

All three calls ended with CallAsync returning false and LastOutboundFailure "Call cancelled by user."

The PBX sends its own ringing. VitalPBX dials extensions with Asterisk's Dial option r, so the caller gets a 180 as soon as the PBX starts dialing, whatever the callee sends. On extension calls behind such a PBX, the event means "the PBX started dialing". The 183 case is covered by the unit tests. A carrier's 180/183 on a trunk call was not measured here.

The silent callee got no CANCEL. Per RFC 3261 a CANCEL can't be sent until the callee has sent a provisional response. The caller's side still ended at the cancel. This is a quirk of the test callee, since real phones send 100 Trying right away.

Other checks on b2f3e35: 106/106 unit tests. SipBotOpen and a pinned consumer build against the branch with the same test outcomes as v0.1.4. The sipbot serve live suite passed 3 runs of 6/6. The sipbot serve JSONL contract is identical to v0.1.4.

@calebtt
calebtt merged commit adb489f into master Sep 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Raise an event when an outbound call starts ringing

1 participant