PjSip.Net.Interop 1.0.9

There is a newer version of this package available.
See the version list below for details.
dotnet add package PjSip.Net.Interop --version 1.0.9
                    
NuGet\Install-Package PjSip.Net.Interop -Version 1.0.9
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PjSip.Net.Interop" Version="1.0.9" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PjSip.Net.Interop" Version="1.0.9" />
                    
Directory.Packages.props
<PackageReference Include="PjSip.Net.Interop" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PjSip.Net.Interop --version 1.0.9
                    
#r "nuget: PjSip.Net.Interop, 1.0.9"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PjSip.Net.Interop@1.0.9
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PjSip.Net.Interop&version=1.0.9
                    
Install as a Cake Addin
#tool nuget:?package=PjSip.Net.Interop&version=1.0.9
                    
Install as a Cake Tool

English | Español

<div align="center">

☎️ PjSip.Net

High-level SIP telephony SDK for .NET 10

NuGet NuGet Downloads Build Native Release License: MIT .NET 10

Based on PJSIP 2.16 with native TLS support (Schannel on Windows, OpenSSL on Android) Compatible with WinForms · WPF · MAUI · Mac Catalyst · Console

</div>


✨ Key Features

Feature Description
📞 Call Management Make, receive, hold, transfer, and record calls
🔒 Native TLS Schannel (Windows), Secure Transport (macOS/iOS), OpenSSL (Android)
👥 Presence & BLF Monitor user availability with SUBSCRIBE/NOTIFY
🎙️ Audio Control Device selection, volume, mute, codec management (G.711, G.729, Opus, Speex, GSM, iLBC)
🔀 Conferencing Multi-party audio bridge with merge/split
💬 SIP Messaging Send and receive text messages via SIP MESSAGE (RFC 3428)
📊 Call Quality Real-time RTP stats, jitter, packet loss, MOS score
🌐 NAT Traversal STUN, ICE, and TURN support built-in
💉 Dependency Injection First-class IServiceCollection integration
📱 Multi-Platform Windows, macOS, Android, iOS from a single API

📑 Table of Contents


📋 Requirements

  • .NET 10 SDK or higher
  • Native package corresponding to your platform (installed automatically via NuGet)

📦 Installation

# Main SDK (required)
dotnet add package PjSip.Net

# Native binaries — install the one for your target platform
dotnet add package PjSip.Net.Native.Win64        # Windows x64
dotnet add package PjSip.Net.Native.MacOS         # macOS x64 / arm64
dotnet add package PjSip.Net.Native.Android       # Android arm64
dotnet add package PjSip.Net.Native.iOS           # iOS arm64

Note: The native package contains the compiled pjsua2 binary and is automatically copied to the output directory.


🚀 Quick Start

using Microsoft.Extensions.DependencyInjection;
using PjSip.Net;
using PjSip.Net.Accounts;
using PjSip.Net.DependencyInjection;
using PjSip.Net.Transport;

// 1. Configure services
var services = new ServiceCollection();
services.AddLogging();
services.AddPjSip(options =>
{
    options.Transports.Add(new SipTransportOptions
    {
        Type = SipTransportType.Udp,
        Port = 5060
    });
    options.Accounts.Add(new SipAccountOptions
    {
        Username = "1001",
        Password = "secret",
        Domain = "pbx.mycompany.com",
        Registrar = "sip:pbx.mycompany.com"
    });
});

// 2. Resolve and start
var provider = services.BuildServiceProvider();
var phone = provider.GetRequiredService<ISipPhone>();

phone.IncomingCall += (s, e) =>
{
    Console.WriteLine($"Incoming call from {e.RemoteUri}");
    e.Call.Answer();  // Answer automatically
};

phone.CallStateChanged += (s, e) =>
    Console.WriteLine($"Call {e.Call.Id}: {e.OldState} -> {e.NewState}");

await phone.StartAsync();

// 3. Make a call
var call = phone.MakeCall(phone.Accounts[0], "sip:1002@pbx.mycompany.com");

// 4. Hang up
call.Hangup();

// 5. Shutdown cleanly
await phone.StopAsync();

⚙️ Configuration

SipPhoneOptions

Global SIP endpoint options. Configured when registering the service.

services.AddPjSip(options =>
{
    options.UserAgent = "MyApp/2.0";        // User-Agent in SIP headers (default: "PjSip.Net/1.0")
    options.LogLevel = 4;                    // PJSIP log level: 0=fatal, 5=trace (default: 4)
    options.MaxCalls = 8;                    // Maximum simultaneous calls (default: 4)
    options.UseCompactForm = false;          // Compact SIP headers (default: false)
    options.CallHistoryMaxEntries = 1000;    // Maximum history entries (default: 1000)
    options.Transports = [ ... ];            // List of transports to create
    options.Accounts = [ ... ];             // Accounts to register on start
    options.Nat = new NatOptions { ... };   // NAT/STUN/ICE/TURN configuration
});
Property Type Default Description
UserAgent string "PjSip.Net/1.0" User-Agent header value in SIP messages
LogLevel int 4 PJSIP internal log verbosity (0-5)
MaxCalls int 4 Maximum number of simultaneous calls
UseCompactForm bool false Use compact form for SIP headers
CallHistoryMaxEntries int 1000 Maximum entries stored in call history
Transports List<SipTransportOptions> [] SIP transports to create on startup
Accounts List<SipAccountOptions> [] SIP accounts to register automatically
Nat NatOptions new() NAT traversal configuration (STUN/ICE/TURN)

SipAccountOptions

Configuration for an individual SIP account.

new SipAccountOptions
{
    Username = "1001",                       // SIP username (required)
    Password = "secret",                     // Password (required)
    Domain = "pbx.mycompany.com",           // SIP domain (required)
    Registrar = "sip:pbx.mycompany.com",    // Registrar URI (null = uses Domain)
    OutboundProxy = "sip:proxy.mycompany.com", // Outbound proxy (null = none)
    DisplayName = "John Doe",               // Display name for caller ID
    Realm = "*",                             // Authentication realm (null = automatic)
    RegistrationTimeout = 300,               // Registration expiration in seconds (default: 300)
    RegisterOnAdd = true,                    // Register automatically when added (default: true)
    UseTls = false                           // Append ;transport=tls to registrar/proxy URIs (default: false)
}
Property Type Default Description
Username string required SIP username for authentication
Password string required Account password
Domain string required SIP domain/server
Registrar string? null Complete registrar URI. If null, constructed from Domain
OutboundProxy string? null Outbound SIP proxy URI. Scheme (sip:/sips:) added automatically if missing
DisplayName string? null Display name in Caller ID
Realm string? null Realm for digest auth. null = accepts any challenge
RegistrationTimeout int 300 REGISTER expiration time in seconds
RegisterOnAdd bool true If true, sends REGISTER automatically when adding account
UseTls bool false Appends ;transport=tls to registrar and proxy URIs

🔐 Transport and TLS

// UDP (unencrypted)
options.Transports.Add(new SipTransportOptions
{
    Type = SipTransportType.Udp,
    Port = 5060
});

// TCP
options.Transports.Add(new SipTransportOptions
{
    Type = SipTransportType.Tcp,
    Port = 5060
});

// TLS (encrypted) — Uses Schannel on Windows, no OpenSSL dependency
options.Transports.Add(new SipTransportOptions
{
    Type = SipTransportType.Tls,
    Port = 5061,
    Tls = new TlsOptions
    {
        VerifyServer = true,               // Validate server certificate (default: true)
        VerifyClient = false,              // Require client certificate (default: false)
        CertificateFile = null,            // Path to client certificate (.pem)
        PrivateKeyFile = null,             // Path to client private key (.pem)
        CaListFile = null                  // Path to additional trusted CAs (.pem)
    }
});

// IPv6
options.Transports.Add(new SipTransportOptions
{
    Type = SipTransportType.Tls6,          // TLS over IPv6
    Port = 5061
});

Available transport types:

Enum Protocol Default Port
SipTransportType.Udp UDP/IPv4 5060
SipTransportType.Tcp TCP/IPv4 5060
SipTransportType.Tls TLS/IPv4 5061
SipTransportType.Udp6 UDP/IPv6 5060
SipTransportType.Tcp6 TCP/IPv6 5060
SipTransportType.Tls6 TLS/IPv6 5061

TlsOptions:

Property Type Default Description
VerifyServer bool true Validate server TLS certificate
VerifyClient bool false Require client TLS certificate
CertificateFile string? null Path to client certificate (PEM format)
PrivateKeyFile string? null Path to client private key (PEM format)
CaListFile string? null Path to additional trusted CA list

Windows: TLS uses Schannel (OS native). No need to install OpenSSL. macOS/iOS: Uses system Secure Transport. Android: TLS enabled via OpenSSL 3.4.1 (statically linked).

🌐 NAT/STUN/ICE/TURN

NAT traversal configuration for networks behind firewalls or NAT routers.

options.Nat = new NatOptions
{
    EnableStun = true,
    StunServers = ["stun.l.google.com:19302", "stun1.l.google.com:19302"],
    EnableIce = true,                        // ICE for media NAT traversal (default: true)
    EnableTurn = false,                      // TURN relay (for symmetric NAT)
    TurnServer = "turn.mycompany.com:3478",
    TurnUsername = "user",
    TurnPassword = "pass",
    TurnTransport = NatTraversalType.Udp,   // TURN transport: Udp, Tcp, Tls
    IceAggressiveNomination = false          // ICE aggressive nomination
};
Property Type Default Description
EnableStun bool false Enable STUN resolution to discover public IP
StunServers List<string> [] List of STUN servers (host:port)
EnableIce bool true Enable ICE for media NAT traversal
EnableTurn bool false Enable TURN relay for symmetric NAT
TurnServer string? null TURN server address (host:port)
TurnUsername string? null Username for TURN authentication
TurnPassword string? null Password for TURN authentication
TurnTransport NatTraversalType Udp TURN transport: Udp, Tcp, Tls
IceAggressiveNomination bool false ICE aggressive nomination (faster, less reliable)

💉 Dependency Injection

Basic registration (Singleton)

services.AddPjSip(options =>
{
    options.Transports.Add(new SipTransportOptions { Type = SipTransportType.Udp });
    options.Accounts.Add(new SipAccountOptions
    {
        Username = "1001",
        Password = "secret",
        Domain = "pbx.mycompany.com"
    });
});

Registration with explicit lifetime

using PjSip.Net.DependencyInjection;

// Singleton (default) — one instance for entire application
services.AddPjSip(options => { ... }, PjSipServiceLifetime.Singleton);

// Scoped — one instance per scope (useful in web applications)
services.AddPjSip(options => { ... }, PjSipServiceLifetime.Scoped);

Registered services

AddPjSip automatically registers:

Service Description
ISipPhone Main facade — manages accounts, calls, and transport
ISipAudioManager Audio device management (microphone, speaker, volume)
ISipCodecManager Audio codec management (priorities, enable/disable)
ISipPresenceManager Presence and BLF (Busy Lamp Field)
ISipMessaging SIP messaging (SIP MESSAGE)
ISipConferenceBridge Audio conferencing (bridge)
ISipCallRecorder Call recording
ISipToneGenerator Tone generator (ringback, busy, dial, DTMF)
ISipCallQualityMonitor Call quality monitoring (RTP stats, MOS)
ISipCallHistory Call history
ISipNetworkMonitor Network change monitoring

Inject into your classes

public class MyTelephonyService
{
    private readonly ISipPhone _phone;

    public MyTelephonyService(ISipPhone phone)
    {
        _phone = phone;
        _phone.IncomingCall += OnIncomingCall;
    }

    public async Task StartAsync()
    {
        await _phone.StartAsync();
    }

    public ISipCall Call(string destination)
    {
        return _phone.MakeCall(_phone.Accounts[0], destination);
    }

    private void OnIncomingCall(object? sender, IncomingCallEventArgs e)
    {
        // Logic for incoming calls
    }
}

You can also inject sub-managers directly:

public class MyPresenceService
{
    private readonly ISipPresenceManager _presence;
    private readonly ISipCallHistory _history;

    public MyPresenceService(ISipPresenceManager presence, ISipCallHistory history)
    {
        _presence = presence;
        _history = history;
    }

    public async Task ShowAvailableAsync()
    {
        await _presence.SetMyPresenceAsync(BuddyState.Online, "Available");
    }

    public int MissedCallsToday()
    {
        return _history.GetMissedCalls().Count;
    }
}

📖 API Reference

ISipPhone

Main SDK facade. Manages the SIP endpoint lifecycle, accounts, calls, and all sub-managers.

public interface ISipPhone : IAsyncDisposable, IDisposable

Properties:

Property Type Description
State SipPhoneState Current phone state
Accounts IReadOnlyList<ISipAccount> Registered SIP accounts
Audio ISipAudioManager Audio device manager
Codecs ISipCodecManager Audio codec manager
Presence ISipPresenceManager Presence and BLF manager
Messaging ISipMessaging SIP messaging (MESSAGE)
Conference ISipConferenceBridge Conference bridge
Recorder ISipCallRecorder Call recorder
Tones ISipToneGenerator Tone generator
Quality ISipCallQualityMonitor Call quality monitor
History ISipCallHistory Call history
Network ISipNetworkMonitor Network change monitor

Methods:

Method Return Description
StartAsync(ct) Task Initializes PJSIP, creates transports and registers configured accounts
StopAsync(ct) Task Hangs up all calls, unregisters accounts and destroys endpoint
AddAccount(options) ISipAccount Adds a new SIP account at runtime
RemoveAccount(account) void Removes and unregisters an account
MakeCall(account, uri) ISipCall Initiates an outgoing call from an account
MakeCall(account, uri, headers) ISipCall Initiates an outgoing call with custom SIP headers

Events:

Event EventArgs Description
IncomingCall IncomingCallEventArgs Incoming call on any account
CallStateChanged CallStateChangedEventArgs State change in any call
RegistrationStateChanged RegistrationStateChangedEventArgs Registration change in any account
TransportStateChanged TransportStateChangedEventArgs Transport state change
MwiStateChanged MwiStateChangedEventArgs New message waiting indicator (voicemail)

States (SipPhoneState):

State Description
Idle Newly created, not initialized
Starting Initializing PJSIP endpoint
Running Operational — can make and receive calls
Stopping Shutting down
Stopped Cleanly stopped
Error Error during startup or shutdown

ISipAccount

Represents a SIP account from which calls can be sent/received.

public interface ISipAccount : IDisposable

Properties:

Property Type Description
Id string Unique account identifier
Uri string SIP URI of the account (e.g., sip:1001@pbx.com)
RegistrationState SipRegistrationState Current registration state
Options SipAccountOptions Account configuration
ActiveCalls IReadOnlyList<ISipCall> Active calls on this account
DndMode DndMode Do Not Disturb mode (read/write)
CallForwarding CallForwardingOptions Call forwarding configuration
MwiInfo MwiInfo? Message waiting information (voicemail), null if no data

Methods:

Method Return Description
RegisterAsync(ct) Task Sends REGISTER to server
UnregisterAsync(ct) Task Sends un-REGISTER to server
MakeCall(destinationUri) ISipCall Initiates a call from this account
MakeCall(destinationUri, headers) ISipCall Initiates a call with custom SIP headers

Events:

Event EventArgs Description
RegistrationStateChanged RegistrationStateChangedEventArgs Registration state change
IncomingCall IncomingCallEventArgs Incoming call for this account
MwiStateChanged MwiStateChangedEventArgs New voicemail state

Registration states (SipRegistrationState):

State Description
Unregistered Not registered
Registering REGISTER sent, waiting for response
Registered Successfully registered (200 OK)
Unregistering Un-REGISTER sent
Error Registration error (401, 403, timeout, etc.)

ISipCall

Represents an active SIP call (incoming or outgoing).

public interface ISipCall : IDisposable

Properties:

Property Type Description
Id string Unique call identifier
State SipCallState Current call state
Direction CallDirection Incoming or Outgoing
Info SipCallInfo Detailed information (URIs, duration, status code)
CustomHeaders IReadOnlyList<SipHeader> Custom SIP headers from the call
IsMuted bool If microphone is muted for this call
IsOnHold bool If call is on hold

Methods:

Method Description
Answer(statusCode) Answer the call. Default: 200 (OK)
Answer(statusCode, headers) Answer with custom SIP headers
Hangup(statusCode) Hang up the call. Default: 603 (Decline)
Hold() Put on hold
Unhold() Remove from hold (re-INVITE)
Transfer(destinationUri) Transfer call to another destination (REFER)
AttendedTransfer(targetCall) Attended transfer — connects this call with another active call
SendDtmf(digits) Send DTMF tones (e.g., "1234#")
SetMute(mute) Mute/unmute microphone

Common response codes for Answer():

Code Meaning
180 Ringing (without answering, only signal ring)
200 OK — answer the call
486 Busy Here — reject as busy
603 Decline — reject the call

Call states (SipCallState):

stateDiagram-v2
    [*] --> Null: Created
    Null --> Calling: MakeCall()
    Null --> Incoming: INVITE received
    Calling --> EarlyMedia: 183 + SDP
    Calling --> Connecting: 200 OK
    Incoming --> Connecting: Answer(200)
    Incoming --> Disconnected: Hangup(603)
    EarlyMedia --> Connecting: 200 OK
    Connecting --> Confirmed: Media ready
    Confirmed --> Disconnected: Hangup / BYE
    Disconnected --> [*]
State Description
Null Newly created call
Calling INVITE sent, waiting for response
Incoming INVITE received, not answered
EarlyMedia Receiving early media (183 + SDP)
Connecting 2xx response received, establishing media
Confirmed Active call with bidirectional audio
Disconnected Call terminated

SipCallInfo (call information):

Property Type Description
CallId string Call-ID from SIP header
RemoteUri string Remote party URI
LocalUri string Local URI
State SipCallState Current state
Direction CallDirection Call direction
Duration TimeSpan Call duration
RemoteDisplayName string? Remote caller display name
StatusCode int Last SIP code received
StatusText string? Text of last SIP status

SipHeader (custom header):

public sealed record SipHeader
{
    public required string Name { get; init; }
    public required string Value { get; init; }
}

ISipAudioManager

Audio device and volume management.

public interface ISipAudioManager

Properties:

Property Type Description
CurrentInputDevice AudioDeviceInfo? Current active microphone
CurrentOutputDevice AudioDeviceInfo? Current active speaker/headset
InputLevel float Input volume level (0.0 — 1.0)
OutputLevel float Output volume level (0.0 — 1.0)

Methods:

Method Return Description
GetInputDevices() IReadOnlyList<AudioDeviceInfo> List of available microphones
GetOutputDevices() IReadOnlyList<AudioDeviceInfo> List of available speakers
SetInputDevice(deviceId) void Change active microphone (immediate mid-call switch)
SetOutputDevice(deviceId) void Change active speaker (immediate mid-call switch)
SetInputDeviceByName(name) bool Change microphone by name (exact then contains, case-insensitive)
SetOutputDeviceByName(name) bool Change speaker by name (exact then contains, case-insensitive)
RefreshDevices() void Invalidate cached device lists so next enumeration re-reads from OS
NotifyAudioRouteChanged(reason, newDeviceName?) void Signal an audio route change from platform listeners

Events:

Event Type Description
AudioRouteChanged EventHandler<AudioRouteChangedEventArgs> Raised on audio route changes (Bluetooth, headset, CarPlay, Android Auto)

AudioRouteChangedEventArgs:

Property Type Description
Reason string Platform-specific reason (e.g., "NewDeviceAvailable", "OldDeviceUnavailable")
NewDeviceName string? Name of the new audio device, if known

AudioDeviceInfo:

Property Type Description
DeviceId int Device ID
Name string Device name (e.g., "Realtek HD Audio")
InputChannels int Number of input channels
OutputChannels int Number of output channels
Driver string? Audio driver name

ISipCodecManager

Audio codec management: list, prioritize, enable and disable.

public interface ISipCodecManager

Methods:

Method Return Description
GetCodecs() IReadOnlyList<CodecInfo> List of available codecs with their priorities
SetCodecPriority(codecId, priority) void Set codec priority (0-255, 0 = disabled)
EnableCodec(codecId, priority) void Enable codec with optional priority (default: 128)
DisableCodec(codecId) void Disable codec (priority = 0)

CodecInfo:

Property Type Description
CodecId string Codec identifier (e.g., "PCMU/8000", "opus/48000", "G729/8000")
Description string Human-readable codec description
Priority int Current priority (0-255, 0 = disabled)
ClockRate int Sampling frequency in Hz
ChannelCount int Number of audio channels

ISipPresenceManager

Presence management (SUBSCRIBE/NOTIFY) and BLF (Busy Lamp Field).

public interface ISipPresenceManager

Properties:

Property Type Description
Buddies IReadOnlyList<ISipBuddy> List of monitored buddies
MyState BuddyState My current presence state

Methods:

Method Return Description
AddBuddy(uri) ISipBuddy Add a buddy to monitor their presence
RemoveBuddy(buddy) void Stop monitoring a buddy
SetMyPresenceAsync(state, statusText, ct) Task Publish my presence state

Events:

Event EventArgs Description
BuddyStateChanged BuddyStateChangedEventArgs Buddy state change

ISipBuddy:

Property/Method Type Description
Uri string Buddy SIP URI
State BuddyState Current state
Info BuddyInfo Complete information (name, state, text, timestamp)
StateChanged event State change notification
SubscribeAsync(ct) Task Subscribe to presence notifications
UnsubscribeAsync(ct) Task Cancel subscription

BuddyState:

State Description
Unknown Unknown state
Online Available
Away Away
Busy Busy
OnThePhone On a call
Offline Offline

ISipMessaging

Sending and receiving SIP messages (MESSAGE method, RFC 3428).

public interface ISipMessaging

Methods:

Method Return Description
SendMessageAsync(account, destUri, body, contentType, ct) Task Send a SIP message. contentType default: "text/plain"

Events:

Event EventArgs Description
MessageReceived SipMessageReceivedEventArgs Message received (contains SipMessage)
MessageStatus SipMessageStatusEventArgs Delivery status of sent message

SipMessage:

Property Type Description
From string Sender URI
To string Recipient URI
Body string Message body
ContentType string Content type (default: "text/plain")
Timestamp DateTime Message time (UTC)

ISipConferenceBridge

Conference bridge for mixing audio from multiple calls.

public interface ISipConferenceBridge

Properties:

Property Type Description
Participants IReadOnlyList<ISipCall> Calls currently in conference

Methods:

Method Return Description
AddParticipant(call) void Add a call to conference
RemoveParticipant(call) void Remove a call from conference
MergeAll(calls) void Merge multiple calls into one conference
SplitAll() void Split all calls from conference

ISipCallRecorder

Call recording to file.

public interface ISipCallRecorder : IDisposable

Properties:

Property Type Description
IsRecording bool If recording is in progress
CurrentFilePath string? Current file path, null if not recording

Methods:

Method Return Description
StartRecording(call, filePath, format) void Start recording. format default: Wav
StopRecording() void Stop current recording

Events:

Event EventArgs Description
RecordingStateChanged RecordingStateChangedEventArgs Recording state change

RecordingFormat:

Value Description
Wav Uncompressed WAV format

ISipToneGenerator

Signaling tone generator.

public interface ISipToneGenerator : IDisposable

Properties:

Property Type Description
IsPlaying bool If a tone is playing

Methods:

Method Return Description
PlayTone(tone) void Play a predefined tone type
PlayTones(tones) void Play a custom tone sequence
PlayRingbackTone() void Ringback tone (North American: 440+480 Hz)
PlayBusyTone() void Busy tone (480+620 Hz)
PlayDialTone() void Dial tone (350+440 Hz)
Stop() void Stop current tone

ToneDescriptor (for custom tones):

Property Type Description
Frequency1 int First frequency in Hz
Frequency2 int Second frequency in Hz (0 = single tone)
OnMs int Tone duration in milliseconds
OffMs int Silence duration in milliseconds
Volume int Volume (default: 16000)

ToneType:

Value Description
Ringback Standard ringback tone
Busy Busy tone
Dial Dial tone
Custom Custom tone

ISipCallQualityMonitor

Call quality monitoring: RTP statistics, jitter, packet loss and MOS score.

public interface ISipCallQualityMonitor

Methods:

Method Return Description
GetQuality(call) CallQualityInfo? Get current call quality (synchronous)
GetQualityAsync(call, ct) Task<CallQualityInfo?> Get current quality (asynchronous, thread-safe)

Events:

Event EventArgs Description
QualityReportAvailable CallQualityEventArgs Quality report available

CallQualityInfo:

Property Type Description
CallId string Call ID
Duration TimeSpan Call duration at measurement time
RtpPacketsSent long Total RTP packets sent
RtpPacketsReceived long Total RTP packets received
RtpPacketsLost long Lost RTP packets
RtpLossPercentage double Packet loss percentage
RtpJitterMs int Jitter in milliseconds
RtpRoundTripTimeMs int Round-trip time in milliseconds
CodecName string? Active codec in call
CodecClockRate int Active codec clock rate
MosScore double Estimated Mean Opinion Score (1.0 — 5.0)

ISipCallHistory

Call history with filtering by type.

public interface ISipCallHistory

Properties:

Property Type Description
Entries IReadOnlyList<CallHistoryEntry> All history entries

Methods:

Method Return Description
GetMissedCalls() IReadOnlyList<CallHistoryEntry> Unanswered incoming calls
GetIncomingCalls() IReadOnlyList<CallHistoryEntry> All incoming calls
GetOutgoingCalls() IReadOnlyList<CallHistoryEntry> All outgoing calls
Clear() void Clear history

Events:

Event EventArgs Description
EntryAdded CallHistoryEntry New entry added to history

CallHistoryEntry:

Property Type Description
CallId string Call ID
RemoteUri string Remote party URI
RemoteDisplayName string? Remote party display name
Direction CallDirection Incoming or Outgoing
StartTime DateTime Start time
EndTime DateTime? End time
Duration TimeSpan Call duration
FinalState SipCallState Final call state
StatusCode int Final SIP code
AccountUri string? Local account URI

ISipNetworkMonitor

Network change monitoring to automatically re-register accounts.

public interface ISipNetworkMonitor : IDisposable

Properties:

Property Type Description
CurrentState NetworkState Current network state

Methods:

Method Return Description
HandleNetworkChangeAsync(ct) Task Manually notify network change (re-registers accounts, restarts transports)

Events:

Event EventArgs Description
NetworkStateChanged NetworkStateChangedEventArgs Network state change

NetworkState:

State Description
Connected Network connected
Disconnected No network connectivity
Changed Network changed (new IP, WiFi/data change)

🔔 Events

All events are fired on the thread that processed the PJSIP callback. In UI applications (WinForms/WPF/MAUI), use the corresponding dispatcher to update the interface.

In ISipPhone (global level)

// Incoming call on any account
phone.IncomingCall += (sender, e) =>
{
    Console.WriteLine($"Call from {e.RemoteDisplayName} <{e.RemoteUri}>");
    Console.WriteLine($"Target account: {e.Account.Uri}");

    e.Call.Answer();           // Answer
    // or: e.Call.Hangup(486);  // Reject as busy
};

// State change in any call
phone.CallStateChanged += (sender, e) =>
{
    Console.WriteLine($"Call {e.Call.Id}: {e.OldState} -> {e.NewState}");

    if (e.NewState == SipCallState.Disconnected)
        Console.WriteLine("Call ended");
};

// Registration change in any account
phone.RegistrationStateChanged += (sender, e) =>
{
    Console.WriteLine($"Account {e.Account.Uri}: {e.OldState} -> {e.NewState}");

    if (e.NewState == SipRegistrationState.Error)
        Console.WriteLine($"Registration error: {e.StatusCode} {e.Reason}");
};

// Transport state change
phone.TransportStateChanged += (sender, e) =>
{
    Console.WriteLine($"Transport {e.TransportType}: {e.State}");
};

// Message Waiting Indicator (voicemail)
phone.MwiStateChanged += (sender, e) =>
{
    Console.WriteLine($"Account {e.Account.Uri}: {e.MwiInfo.NewMessages} new message(s)");
};

In ISipAccount (account level)

var account = phone.Accounts[0];

account.RegistrationStateChanged += (sender, e) =>
    Console.WriteLine($"My account: {e.NewState}");

account.IncomingCall += (sender, e) =>
    Console.WriteLine($"Incoming call for this account: {e.RemoteUri}");

account.MwiStateChanged += (sender, e) =>
    Console.WriteLine($"Voicemail: {e.MwiInfo.NewMessages} new, {e.MwiInfo.OldMessages} old");

In ISipCall (call level)

var call = phone.MakeCall(account, "sip:1002@pbx.com");

call.StateChanged += (sender, e) =>
{
    Console.WriteLine($"State: {e.OldState} -> {e.NewState}");

    if (e.NewState == SipCallState.Confirmed)
        Console.WriteLine("Audio active!");
};

call.MediaStateChanged += (sender, e) =>
{
    Console.WriteLine($"Media active: {e.IsActive}");
};

🧩 Advanced Features

👥 Presence and BLF

Monitoring presence state of other users (Busy Lamp Field).

var presence = phone.Presence;

// Publish my state
await presence.SetMyPresenceAsync(BuddyState.Online, "Available");

// Add a buddy to monitor
var buddy = presence.AddBuddy("sip:1002@pbx.com");
await buddy.SubscribeAsync();

// Listen to state changes
buddy.StateChanged += (s, e) =>
    Console.WriteLine($"{buddy.Uri}: {e.OldState} -> {e.NewState}");

// Also at global level
presence.BuddyStateChanged += (s, e) =>
    Console.WriteLine($"Buddy {e.Buddy.Uri}: {e.NewState}");

// Query current state
Console.WriteLine($"Current state: {buddy.State}");
Console.WriteLine($"Last updated: {buddy.Info.LastUpdated}");

// Stop monitoring
await buddy.UnsubscribeAsync();
presence.RemoveBuddy(buddy);

💬 SIP Messaging (MESSAGE)

Sending and receiving text messages via SIP MESSAGE (RFC 3428).

var messaging = phone.Messaging;

// Send a message
await messaging.SendMessageAsync(
    phone.Accounts[0],
    "sip:1002@pbx.com",
    "Hello, are you available for a call?"
);

// Receive messages
messaging.MessageReceived += (s, e) =>
    Console.WriteLine($"Message from {e.Message.From}: {e.Message.Body}");

// Delivery status
messaging.MessageStatus += (s, e) =>
    Console.WriteLine($"Message to {e.DestinationUri}: code {e.StatusCode}");

🔀 Conference

Mixing audio from multiple calls into a conference.

var conference = phone.Conference;
var account = phone.Accounts[0];

// Create calls
var call1 = phone.MakeCall(account, "sip:1002@pbx.com");
var call2 = phone.MakeCall(account, "sip:1003@pbx.com");

// Wait until connected, then merge
conference.AddParticipant(call1);
conference.AddParticipant(call2);

// View participants
Console.WriteLine($"Participants: {conference.Participants.Count}");

// Merge all at once
conference.MergeAll(new[] { call1, call2 });

// Split all
conference.SplitAll();

// Remove one
conference.RemoveParticipant(call2);

🎙️ Call Recording

Recording call audio to file.

var recorder = phone.Recorder;

// Start recording
recorder.StartRecording(call, @"C:\recordings\call-001.wav");

// Check status
Console.WriteLine($"Recording: {recorder.IsRecording}");
Console.WriteLine($"File: {recorder.CurrentFilePath}");

// Listen to state changes
recorder.RecordingStateChanged += (s, e) =>
    Console.WriteLine($"Recording: {(e.IsRecording ? "started" : "stopped")}");

// Stop recording
recorder.StopRecording();

🎵 Tone Generator

Playing standard or custom signaling tones.

var tones = phone.Tones;

// Standard tones (North American frequencies)
tones.PlayDialTone();      // 350+440 Hz continuous
tones.PlayRingbackTone();  // 440+480 Hz, 2s on / 4s off
tones.PlayBusyTone();      // 480+620 Hz, 0.5s on / 0.5s off

// Generic tone by type
tones.PlayTone(ToneType.Busy);

// Custom tones
tones.PlayTones(new[]
{
    new ToneDescriptor { Frequency1 = 941, Frequency2 = 1336, OnMs = 100, OffMs = 100 },  // '#' key
    new ToneDescriptor { Frequency1 = 697, Frequency2 = 1209, OnMs = 100, OffMs = 100 },  // '1' key
});

// Stop
tones.Stop();
Console.WriteLine($"Playing: {tones.IsPlaying}");

📊 Call Quality

Monitoring RTP statistics and MOS score during an active call.

var quality = phone.Quality;

// Query quality of an active call
var info = quality.GetQuality(call);
if (info != null)
{
    Console.WriteLine($"Codec: {info.CodecName}");
    Console.WriteLine($"Packets sent: {info.RtpPacketsSent}");
    Console.WriteLine($"Loss: {info.RtpLossPercentage:F1}%");
    Console.WriteLine($"Jitter: {info.RtpJitterMs}ms");
    Console.WriteLine($"RTT: {info.RtpRoundTripTimeMs}ms");
    Console.WriteLine($"MOS: {info.MosScore:F1}/5.0");
}

// Or asynchronously (thread-safe)
var asyncInfo = await quality.GetQualityAsync(call);

// Listen to periodic reports
quality.QualityReportAvailable += (s, e) =>
    Console.WriteLine($"Call {e.Call.Id}: MOS={e.Quality.MosScore:F1}");

📋 Call History

Automatic call history with filtering.

var history = phone.History;

// History is filled automatically when a call disconnects

// Query all entries
foreach (var entry in history.Entries)
{
    Console.WriteLine($"[{entry.Direction}] {entry.RemoteUri} - " +
                      $"{entry.Duration:mm\\:ss} - {entry.FinalState}");
}

// Filter by type
var missed = history.GetMissedCalls();
var incoming = history.GetIncomingCalls();
var outgoing = history.GetOutgoingCalls();

Console.WriteLine($"Missed: {missed.Count}");
Console.WriteLine($"Incoming: {incoming.Count}");
Console.WriteLine($"Outgoing: {outgoing.Count}");

// Listen to new entries
history.EntryAdded += (s, entry) =>
    Console.WriteLine($"New entry: {entry.RemoteUri} ({entry.Direction})");

// Clear history
history.Clear();

Maximum history size is configured with SipPhoneOptions.CallHistoryMaxEntries (default: 1000).

🔕 Do Not Disturb (DND)

Control incoming call behavior per account.

var account = phone.Accounts[0];

// Activate DND — reject all calls
account.DndMode = DndMode.RejectAll;

// Reject with busy signal (486 Busy Here)
account.DndMode = DndMode.RejectWithBusy;

// Silent ring (call arrives but without tone)
account.DndMode = DndMode.SilentRing;

// Disable DND
account.DndMode = DndMode.Off;

DND modes (DndMode):

Mode Description
Off Disabled — normal behavior
RejectAll Rejects all incoming calls (603 Decline)
RejectWithBusy Rejects with busy signal (486 Busy Here)
SilentRing Call arrives but doesn't ring (silent ring)

↪️ Call Forwarding

Configure call forwarding for an account.

var account = phone.Accounts[0];

// Unconditional forwarding
account.CallForwarding.Enabled = true;
account.CallForwarding.Type = CallForwardingType.Unconditional;
account.CallForwarding.DestinationUri = "sip:1003@pbx.com";

// Forward on no answer (after 20 seconds)
account.CallForwarding.Type = CallForwardingType.OnNoAnswer;
account.CallForwarding.NoAnswerTimeout = TimeSpan.FromSeconds(20);

// Forward on busy
account.CallForwarding.Type = CallForwardingType.OnBusy;

// Disable
account.CallForwarding.Enabled = false;

Forwarding types (CallForwardingType):

Type Description
Unconditional Forwards all calls immediately
OnBusy Forwards if account is busy
OnNoAnswer Forwards if not answered within configured timeout
OnNotReachable Forwards if account is not available

📬 Message Waiting Indicator (MWI)

Receiving voicemail notifications.

var account = phone.Accounts[0];

// Listen to MWI changes at account level
account.MwiStateChanged += (s, e) =>
{
    Console.WriteLine($"Voicemail updated:");
    Console.WriteLine($"  Has waiting: {e.MwiInfo.HasWaiting}");
    Console.WriteLine($"  New: {e.MwiInfo.NewMessages}");
    Console.WriteLine($"  Old: {e.MwiInfo.OldMessages}");
    Console.WriteLine($"  New urgent: {e.MwiInfo.NewUrgentMessages}");
    Console.WriteLine($"  Old urgent: {e.MwiInfo.OldUrgentMessages}");
};

// Or at global level
phone.MwiStateChanged += (s, e) =>
    Console.WriteLine($"Account {e.Account.Uri}: {e.MwiInfo.NewMessages} new");

// Query current state (null if no notification received yet)
var mwi = account.MwiInfo;
if (mwi != null && mwi.HasWaiting)
    Console.WriteLine($"You have {mwi.NewMessages} voice message(s)");

MwiInfo:

Property Type Description
HasWaiting bool If messages are waiting
NewMessages int Number of new messages
OldMessages int Number of already listened messages
NewUrgentMessages int New urgent messages
OldUrgentMessages int Already listened urgent messages
AccountUri string? Associated account URI

🏷️ Custom Headers

Sending custom SIP headers in calls.

using PjSip.Net.Calls;

var headers = new[]
{
    new SipHeader { Name = "X-Tenant-Id", Value = "acme-corp" },
    new SipHeader { Name = "X-Call-Tag", Value = "support-level2" }
};

// In MakeCall
var call = phone.MakeCall(account, "sip:1002@pbx.com", headers);

// Or from account
var call2 = account.MakeCall("sip:1003@pbx.com", headers);

// Read headers from a call
foreach (var h in call.CustomHeaders)
    Console.WriteLine($"{h.Name}: {h.Value}");

// When answering with headers
call.Answer(200, new[]
{
    new SipHeader { Name = "X-Agent-Id", Value = "42" }
});

🤝 Attended Transfer

Connecting two active calls (transfer with prior consultation).

sequenceDiagram
    participant Agent
    participant Customer
    participant Specialist

    Agent->>Customer: Active call
    Agent->>Agent: Hold(customer)
    Agent->>Specialist: MakeCall()
    Note over Agent,Specialist: Consultation
    Agent->>Customer: AttendedTransfer(specialist)
    Customer-->>Specialist: Connected directly
    Note over Agent: Both calls disconnect
// Active call with customer
var callCustomer = phone.MakeCall(account, "sip:customer@example.com");
// ... customer is on the line ...

// Put customer on hold
callCustomer.Hold();

// Call specialist for consultation
var callSpecialist = phone.MakeCall(account, "sip:specialist@example.com");
// ... talk to specialist ...

// Connect customer with specialist (attended transfer)
callCustomer.AttendedTransfer(callSpecialist);
// Both calls disconnect from agent; customer and specialist remain connected

🌐 Network Monitor

Detecting network changes and automatically re-registering accounts.

var network = phone.Network;

// Current state
Console.WriteLine($"Network: {network.CurrentState}");

// Listen to changes
network.NetworkStateChanged += (s, e) =>
{
    Console.WriteLine($"Network changed: {e.OldState} -> {e.NewState}");

    if (e.NewState == NetworkState.Disconnected)
        Console.WriteLine("No network connectivity");
};

// Manually notify network change (e.g., from OS events)
await network.HandleNetworkChangeAsync();

📱 Platform Examples

Console App

using Microsoft.Extensions.DependencyInjection;
using PjSip.Net;
using PjSip.Net.Accounts;
using PjSip.Net.DependencyInjection;
using PjSip.Net.Transport;

var services = new ServiceCollection();
services.AddLogging();
services.AddPjSip(options =>
{
    options.Transports.Add(new SipTransportOptions
    {
        Type = SipTransportType.Tls,
        Port = 5061
    });
    options.Accounts.Add(new SipAccountOptions
    {
        Username = "1001",
        Password = "secret",
        Domain = "pbx.mycompany.com",
        Registrar = "sip:pbx.mycompany.com"
    });
});

var provider = services.BuildServiceProvider();
var phone = provider.GetRequiredService<ISipPhone>();

phone.IncomingCall += (s, e) =>
{
    Console.WriteLine($"Call from {e.RemoteUri}");
    e.Call.Answer();
};

await phone.StartAsync();
Console.WriteLine("Phone active. Press Enter to exit...");
Console.ReadLine();
await phone.StopAsync();

WPF

// In App.xaml.cs or with a HostBuilder
public partial class App : Application
{
    private ServiceProvider? _serviceProvider;

    protected override async void OnStartup(StartupEventArgs e)
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddPjSip(options =>
        {
            options.Transports.Add(new SipTransportOptions
            {
                Type = SipTransportType.Tls,
                Port = 5061
            });
            options.Accounts.Add(new SipAccountOptions
            {
                Username = "1001",
                Password = "secret",
                Domain = "pbx.mycompany.com"
            });
        });

        _serviceProvider = services.BuildServiceProvider();
        var mainWindow = new MainWindow(_serviceProvider.GetRequiredService<ISipPhone>());
        mainWindow.Show();
    }
}

// In MainWindow.xaml.cs
public partial class MainWindow : Window
{
    private readonly ISipPhone _phone;
    private ISipCall? _activeCall;

    public MainWindow(ISipPhone phone)
    {
        InitializeComponent();
        _phone = phone;

        // IMPORTANT: Use Dispatcher to update UI from SIP events
        _phone.IncomingCall += (s, e) =>
            Dispatcher.Invoke(() =>
            {
                StatusText.Text = $"Incoming call from {e.RemoteDisplayName}";
                // Show accept/reject dialog
            });

        _phone.CallStateChanged += (s, e) =>
            Dispatcher.Invoke(() =>
                StatusText.Text = $"Call: {e.NewState}");
    }

    private async void OnStartClick(object sender, RoutedEventArgs e)
    {
        await _phone.StartAsync();
        StatusText.Text = $"Connected ({_phone.Accounts.Count} accounts)";
    }

    private void OnCallClick(object sender, RoutedEventArgs e)
    {
        _activeCall = _phone.MakeCall(_phone.Accounts[0], DestinationBox.Text);
    }

    private void OnHangupClick(object sender, RoutedEventArgs e)
    {
        _activeCall?.Hangup();
        _activeCall = null;
    }
}

WinForms

public partial class MainForm : Form
{
    private ISipPhone? _phone;

    private async Task StartPhoneAsync()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddPjSip(options =>
        {
            options.Transports.Add(new SipTransportOptions { Type = SipTransportType.Udp });
            options.Accounts.Add(new SipAccountOptions
            {
                Username = "1001",
                Password = "secret",
                Domain = "pbx.mycompany.com"
            });
        });

        var provider = services.BuildServiceProvider();
        _phone = provider.GetRequiredService<ISipPhone>();

        // IMPORTANT: Use BeginInvoke to update UI
        _phone.IncomingCall += (s, e) =>
            BeginInvoke(() =>
                MessageBox.Show($"Call from {e.RemoteUri}", "Incoming Call"));

        _phone.CallStateChanged += (s, e) =>
            BeginInvoke(() =>
                lblStatus.Text = $"Call: {e.NewState}");

        await _phone.StartAsync();
        lblStatus.Text = "Connected";
    }

    private void btnCall_Click(object sender, EventArgs e)
    {
        _phone?.MakeCall(_phone.Accounts[0], txtDestination.Text);
    }
}

MAUI

// In MauiProgram.cs
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.UseMauiApp<App>();

        builder.Services.AddPjSip(options =>
        {
            options.Transports.Add(new SipTransportOptions
            {
                Type = SipTransportType.Udp
            });
            options.Accounts.Add(new SipAccountOptions
            {
                Username = "1001",
                Password = "secret",
                Domain = "pbx.mycompany.com"
            });
        });

        return builder.Build();
    }
}

// In a page
public partial class PhonePage : ContentPage
{
    private readonly ISipPhone _phone;

    public PhonePage(ISipPhone phone)
    {
        InitializeComponent();
        _phone = phone;

        // MAUI: Use MainThread.BeginInvokeOnMainThread for UI
        _phone.IncomingCall += (s, e) =>
            MainThread.BeginInvokeOnMainThread(() =>
                StatusLabel.Text = $"Call from {e.RemoteUri}");
    }

    private async void OnStartClicked(object? sender, EventArgs e)
    {
        await _phone.StartAsync();
        StatusLabel.Text = "Connected";
    }

    private void OnCallClicked(object? sender, EventArgs e)
    {
        _phone.MakeCall(_phone.Accounts[0], DestinationEntry.Text);
    }
}

⚠️ Error Handling

The SDK defines specific exceptions for SIP errors:

using PjSip.Net.Exceptions;

try
{
    await phone.StartAsync();
}
catch (SipTransportException ex)
{
    // Transport creation error (port busy, misconfigured TLS, etc.)
    Console.WriteLine($"Transport error: {ex.Message} (PJSIP code: {ex.PjStatusCode})");
}
catch (PjSipException ex)
{
    // Generic PJSIP error
    Console.WriteLine($"PJSIP error: {ex.Message} (code: {ex.PjStatusCode})");
}

// Registration errors are notified via event
phone.RegistrationStateChanged += (s, e) =>
{
    if (e.NewState == SipRegistrationState.Error)
    {
        // e.StatusCode contains SIP code (401, 403, 408, etc.)
        Console.WriteLine($"Registration error: {e.StatusCode} - {e.Reason}");
    }
};

Exception hierarchy:

PjSipException                    Base — any PJSIP error
├── SipRegistrationException      REGISTER error (4xx, 5xx)
└── SipTransportException         Transport error (bind, TLS, network)

🎧 Audio

var audio = phone.Audio;

// List devices
var microphones = audio.GetInputDevices();
var speakers = audio.GetOutputDevices();

foreach (var mic in microphones)
    Console.WriteLine($"[{mic.DeviceId}] {mic.Name} ({mic.InputChannels}ch)");

foreach (var spk in speakers)
    Console.WriteLine($"[{spk.DeviceId}] {spk.Name} ({spk.OutputChannels}ch)");

// Change device by ID (works mid-call — immediate switch)
audio.SetInputDevice(microphones[1].DeviceId);
audio.SetOutputDevice(speakers[0].DeviceId);

// Change device by name (exact match then contains, case-insensitive)
audio.SetInputDeviceByName("Realtek");
audio.SetOutputDeviceByName("Jabra");

// Adjust volume (0.0 = silence, 1.0 = maximum)
audio.InputLevel = 0.8f;    // Microphone at 80%
audio.OutputLevel = 1.0f;   // Speaker at 100%

// Mute a specific call
call.SetMute(true);   // Mute microphone for this call
call.SetMute(false);  // Unmute

// React to audio route changes (Bluetooth, headset, CarPlay, Android Auto)
audio.AudioRouteChanged += (s, e) =>
{
    Console.WriteLine($"Audio route changed: {e.Reason}, device: {e.NewDeviceName}");
    // Optionally switch to the new device
    if (e.NewDeviceName is not null)
        audio.SetOutputDeviceByName(e.NewDeviceName);
};

// Notify from platform-specific listener (e.g., iOS AVAudioSession)
audio.NotifyAudioRouteChanged("NewDeviceAvailable", "AirPods Pro");

// Force refresh device list after hardware changes
audio.RefreshDevices();

🔧 Low-Level Access (pjsua2)

For advanced scenarios requiring direct access to pjsua2 classes generated by SWIG:

// SWIG classes are in the PjSip.Net.Interop.Generated namespace
using PjSip.Net.Interop.Generated;

// Example: access native endpoint directly
// (available once SWIG wrappers are generated)

Note: Low-level access requires knowledge of the pjsua2 API. See the official PJSIP documentation.


🏗️ Architecture

graph TB
    subgraph App["Your Application"]
        WF[WinForms]
        WPF[WPF]
        MAUI[MAUI]
        CON[Console]
    end

    subgraph SDK["PjSip.Net — High-level SDK"]
        Phone[ISipPhone]
        Account[ISipAccount]
        Call[ISipCall]
        Audio[ISipAudioManager]
        Codec[ISipCodecManager]
        Presence[ISipPresenceManager]
        Msg[ISipMessaging]
        Conf[ISipConferenceBridge]
        Rec[ISipCallRecorder]
        Tone[ISipToneGenerator]
        QoS[ISipCallQualityMonitor]
        Hist[ISipCallHistory]
        Net[ISipNetworkMonitor]
        DI["DI (AddPjSip)"]
        Events["Events"]
    end

    subgraph Interop["PjSip.Net.Interop"]
        Loader[NativeLoader]
        SWIG["Generated/ (SWIG C#)"]
    end

    subgraph Native["PjSip.Net.Native.{Platform}"]
        Win["Win64 — pjsua2.dll"]
        Mac["MacOS — libpjsua2.dylib"]
        And["Android — libpjsua2.so"]
        iOS["iOS — libpjsua2.dylib"]
    end

    App --> SDK
    SDK --> Interop
    Interop --> Native

    style App fill:#e1f5fe
    style SDK fill:#f3e5f5
    style Interop fill:#fff3e0
    style Native fill:#e8f5e9

Design Patterns used:

Pattern Use
Facade ISipPhone as single entry point with 11 sub-managers
Options SipPhoneOptions, SipAccountOptions, NatOptions via IOptions<T>
Observer .NET events (IncomingCall, CallStateChanged, BuddyStateChanged, etc.)
Factory AddAccount(), MakeCall(), AddBuddy()
Adapter ManagedAccount/ManagedCall/ManagedBuddy adapt pjsua2 callbacks to .NET events
Dispose Cascading cleanup of native resources

📱 Supported Platforms

Platform RID TLS Backend Native Package
<img src="https://img.shields.io/badge/Windows-0078D6?logo=windows&logoColor=white" alt="Windows" /> win-x64 Schannel PjSip.Net.Native.Win64
<img src="https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=white" alt="macOS" /> x64 osx-x64 Apple SSL PjSip.Net.Native.MacOS
<img src="https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=white" alt="macOS" /> ARM64 osx-arm64 Apple SSL PjSip.Net.Native.MacOS
<img src="https://img.shields.io/badge/Mac_Catalyst-000000?logo=apple&logoColor=white" alt="Mac Catalyst" /> osx-arm64 / osx-x64 Apple SSL PjSip.Net.Native.MacOS
<img src="https://img.shields.io/badge/Android-3DDC84?logo=android&logoColor=white" alt="Android" /> ARM64 android-arm64 OpenSSL 3.4.1 PjSip.Net.Native.Android
<img src="https://img.shields.io/badge/iOS-000000?logo=ios&logoColor=white" alt="iOS" /> ARM64 ios-arm64 Secure Transport PjSip.Net.Native.iOS

🛠️ Build from Source

Prerequisites

  • .NET 10 SDK
  • Visual Studio 2022 with C++ workload (to compile pjsua2 on Windows)
  • SWIG 4.0+ (to generate C# wrappers)

Compile managed solution

dotnet build PjSip.Net.slnx
dotnet test tests/PjSip.Net.Tests.Unit/PjSip.Net.Tests.Unit.csproj

Compile native binaries

# Windows x64 (PowerShell)
./native/build-win64.ps1

# macOS (bash)
./native/build-macos.sh

# Mobile (bash)
./native/build-android.sh
./native/build-ios.sh

Create NuGet packages

dotnet pack src/PjSip.Net/PjSip.Net.csproj -o ./artifacts
dotnet pack src/PjSip.Net.Interop/PjSip.Net.Interop.csproj -o ./artifacts
dotnet pack src/PjSip.Net.Native.Win64/PjSip.Net.Native.Win64.csproj -o ./artifacts

📝 Changelog

v1.0.3

  • Feature: Account-specific buddy subscriptions — AddBuddy(uri, account) overload allows binding presence subscriptions to a specific SIP account instead of always using the primary account

v1.0.2

  • Fix: Populate RemoteDisplayName on incoming calls — the display name from the SIP From header is now correctly extracted when the call is first created
  • Fix: Preserve RemoteDisplayName across call state updates — on onCallState callbacks, the display name is retained even if the native CallInfo no longer includes it

v1.0.1

  • Feature: Outbound proxy support (SipAccountOptions.OutboundProxy)
  • Feature: TLS transport suffix — automatic ;transport=tls append via SipAccountOptions.UseTls

v1.0.0

  • First stable production release

📄 License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on PjSip.Net.Interop:

Package Downloads
PjSip.Net

High-level .NET SDK for PJSIP — SIP phone, accounts, calls, TLS, and DI support for WinForms, WPF, and MAUI

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.17 125 3/20/2026
1.0.16 105 3/20/2026
1.0.15 103 3/20/2026
1.0.14 106 3/20/2026
1.0.13 123 3/11/2026
1.0.12 127 2/26/2026
1.0.11 115 2/26/2026
1.0.10 115 2/26/2026
1.0.9 116 2/26/2026
1.0.8 115 2/26/2026
1.0.7 114 2/26/2026
1.0.6 117 2/24/2026
1.0.5 122 2/24/2026
1.0.4 113 2/24/2026
1.0.3 120 2/24/2026
1.0.2 111 2/24/2026
1.0.1 115 2/24/2026
1.0.0 112 2/23/2026
1.0.0-rc.13 67 2/23/2026
1.0.0-rc.12 64 2/23/2026
Loading failed