Calls
A call (ICall) is one SIP dialog and its media session. You obtain one from
line.DialAsync(...) (outbound) or the IncomingCall/OnIncomingCall surface
(inbound). client.Calls (CallManager) tracks the active set.
Control surface
Methods that throw InvalidOperationException when the call is in the wrong state
(they represent a programmer error if misused):
await call.AcceptAsync(); // answer an inbound call (200 OK)
await call.HangupAsync(); // end the call (BYE)
await call.HoldAsync(); // re-INVITE, sendonly
await call.UnholdAsync(); // re-INVITE, sendrecv
await call.SendDtmfAsync(DtmfTone.Five); // RFC 4733 telephone-event
await call.BlindTransferAsync("sip:1003@pbx.example.com");
bool ok = await call.AttendedTransferAsync(consultationCall);
AttendedTransferAsyncis the exception to the rule above: it currently has no state guard, so a wrong-state call is not rejected — invoke it only fromConnected.
Mute this call's outgoing audio
MuteAsync gates this call's outgoing audio locally — the SDK stops (or resumes) sending its
captured audio to the peer:
await call.MuteAsync(true); // stop sending this call's audio to the peer
bool muted = call.IsMuted; // true
await call.MuteAsync(false); // resume
Unlike HoldAsync it is not signalled to the peer (no re-INVITE), and unlike the device-wide
IVoipClient.SetAudioInputMuted — which mutes the capture device for every call — it affects
only this call, so concurrent calls mute independently. It is outgoing (microphone) mute:
inbound audio is unaffected. Being local, it is valid in any live state and does not throw for state;
it is a no-op when the call is already in the requested state.
Methods that return a CallActionResult instead of throwing for foreseeable outcomes
(remote decline, invalid request, timeout):
CallActionResult r = await call.RejectAsync(/* … */);
await call.RedirectAsync(/* … */);
await call.SendInfoAsync(/* … */);
await call.SendOptionsAsync();
await call.SendSubscribeAsync(/* … */);
await call.SendNotifyAsync(/* … */);
The full rule is the error contract.
Call events
| Event | Meaning | Thread |
|---|---|---|
StateChanged |
Dialog state transition | Signaling (serialized) |
HoldStateChanged |
Local/remote hold state | Signaling (serialized) |
DtmfReceived |
Inbound DTMF | SIP INFO or RFC-4733 media thread |
TransferRequested |
Peer asks for a transfer (REFER) | Signaling (synchronous accept/reject) |
QualitySnapshotChanged |
New RTCP quality snapshot | Media/RTCP thread |
See Events for the threading contract these follow. Subscribe before placing or accepting a call — events are delivered live to the handlers registered at the time of the transition and are not guaranteed to be replayed to a handler that subscribes afterwards.
Why a call ended
Once a call reaches Terminated, ICall.TerminationReason explains why — so a busy, unanswered,
cancelled or rejected call is distinguishable from a generic failure. The same value is on the
terminating StateChanged event (CallStateChangedEventArgs.TerminationReason).
call.StateChanged += (_, e) =>
{
if (e.NewState != CallState.Terminated) return;
var reason = e.TerminationReason;
Console.WriteLine($"{reason?.Category} ({reason?.SipStatusCode} {reason?.ReasonPhrase}), " +
$"ended by {reason?.TerminatedBy}");
};
CallTerminationReason carries the SipStatusCode and ReasonPhrase, a protocol-neutral
Category (busy, no answer, cancelled, rejected, …), TerminatedBy (local or remote) and
RetryAfterSeconds where the peer supplied one. The classification follows the authoritative SIP
response status (RFC 3261 §21), not the advisory Q.850 Reason header — the same choice PJSIP,
SIPSorcery and Twilio make.
Inbound caller and dialed identity
An inbound call surfaces both who is calling and which number they dialed, parsed once by the SDK so you do
not re-parse SIP headers yourself. All four properties are read-only and null on outbound calls (and when
the data is absent):
| Property | Meaning |
|---|---|
CalledNumber |
The dialed number (DID) the call was addressed to — the To/Request-URI user part, i.e. the number that selected the receiving trunk line. |
LocalParty |
The local party's SIP URI in this dialog, parallel to RemoteParty (on inbound the called/DID URI). |
RemoteNumber |
The caller's number — the user part of RemoteParty. |
RemoteDisplayName |
The caller's display name from the inbound From header (RFC 3261 §8.1.1.3), or null when none was sent. |
Typical use is a screen-pop and trunk DID routing — show the caller and branch on the DID the call arrived on:
client.OnIncomingCall(async call =>
{
ScreenPop(
dialedDid: call.CalledNumber, // route by which DID / line the call came in on
callerNumber: call.RemoteNumber,
callerName: call.RemoteDisplayName);
await call.AcceptAsync();
});
These are informational identity as received. For a trust-gated caller identity use RemoteAssertedIdentity
(P-Asserted-Identity, RFC 3325).
Media
Each call has a media session. Attach the default audio device with
client.AttachDefaultAudioAsync(call), or attach a media tap
for bots and streaming. Negotiated encryption (SRTP/SRTCP) is transparent to this
surface — see SRTP/SRTCP.